@taskforcehq/taskforce 0.3.303 → 0.3.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -830,6 +830,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
830
830
|
});
|
|
831
831
|
it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
|
|
832
832
|
const fetchMock = vi.fn((url) => {
|
|
833
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
834
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
835
|
+
}
|
|
833
836
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
834
837
|
return Promise.resolve({
|
|
835
838
|
ok: true,
|
|
@@ -929,8 +932,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
929
932
|
json: () => Promise.resolve({
|
|
930
933
|
plans: [
|
|
931
934
|
{
|
|
932
|
-
planId: '
|
|
933
|
-
displayName: '
|
|
935
|
+
planId: 'basic',
|
|
936
|
+
displayName: 'Basic Plan test mode',
|
|
934
937
|
status: 'active',
|
|
935
938
|
features: [],
|
|
936
939
|
pricing: { month: null, year: null },
|
|
@@ -946,7 +949,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
946
949
|
render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
|
|
947
950
|
await waitFor(() => {
|
|
948
951
|
expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
|
|
949
|
-
expect(screen.getByText('
|
|
952
|
+
expect(screen.getByText('Basic Plan test mode')).toBeInTheDocument();
|
|
950
953
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
951
954
|
});
|
|
952
955
|
});
|
|
@@ -1019,8 +1022,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1019
1022
|
json: () => Promise.resolve({
|
|
1020
1023
|
plans: [
|
|
1021
1024
|
{
|
|
1022
|
-
planId: '
|
|
1023
|
-
displayName: '
|
|
1025
|
+
planId: 'free',
|
|
1026
|
+
displayName: 'Free',
|
|
1024
1027
|
status: 'active',
|
|
1025
1028
|
features: [],
|
|
1026
1029
|
pricing: {
|
|
@@ -1028,7 +1031,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1028
1031
|
year: null
|
|
1029
1032
|
},
|
|
1030
1033
|
defaultVersion: {
|
|
1031
|
-
planVersionId: '
|
|
1034
|
+
planVersionId: 'free-v1',
|
|
1032
1035
|
versionNumber: 1,
|
|
1033
1036
|
seatLimit: null,
|
|
1034
1037
|
metadata: {}
|
|
@@ -1042,8 +1045,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1042
1045
|
return Promise.resolve({
|
|
1043
1046
|
ok: true,
|
|
1044
1047
|
json: () => Promise.resolve({
|
|
1045
|
-
planId: '
|
|
1046
|
-
planName: '
|
|
1048
|
+
planId: 'free',
|
|
1049
|
+
planName: 'Free',
|
|
1047
1050
|
entitlementState: 'active'
|
|
1048
1051
|
})
|
|
1049
1052
|
});
|
|
@@ -1052,8 +1055,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1052
1055
|
return Promise.resolve({
|
|
1053
1056
|
ok: true,
|
|
1054
1057
|
json: () => Promise.resolve({
|
|
1055
|
-
planId: '
|
|
1056
|
-
planName: '
|
|
1058
|
+
planId: 'free',
|
|
1059
|
+
planName: 'Free',
|
|
1057
1060
|
entitlementState: 'active'
|
|
1058
1061
|
})
|
|
1059
1062
|
});
|
|
@@ -1154,8 +1157,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1154
1157
|
json: () => Promise.resolve({
|
|
1155
1158
|
plans: [
|
|
1156
1159
|
{
|
|
1157
|
-
planId: '
|
|
1158
|
-
displayName: '
|
|
1160
|
+
planId: 'free',
|
|
1161
|
+
displayName: 'Free',
|
|
1159
1162
|
status: 'active',
|
|
1160
1163
|
features: [],
|
|
1161
1164
|
pricing: {
|
|
@@ -1163,7 +1166,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1163
1166
|
year: null
|
|
1164
1167
|
},
|
|
1165
1168
|
defaultVersion: {
|
|
1166
|
-
planVersionId: '
|
|
1169
|
+
planVersionId: 'free-v1',
|
|
1167
1170
|
versionNumber: 1,
|
|
1168
1171
|
seatLimit: null,
|
|
1169
1172
|
metadata: {}
|
|
@@ -1269,8 +1272,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1269
1272
|
json: () => Promise.resolve({
|
|
1270
1273
|
plans: [
|
|
1271
1274
|
{
|
|
1272
|
-
planId: '
|
|
1273
|
-
displayName: '
|
|
1275
|
+
planId: 'free',
|
|
1276
|
+
displayName: 'Free',
|
|
1274
1277
|
status: 'active',
|
|
1275
1278
|
features: [],
|
|
1276
1279
|
pricing: {
|
|
@@ -1278,7 +1281,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1278
1281
|
year: null
|
|
1279
1282
|
},
|
|
1280
1283
|
defaultVersion: {
|
|
1281
|
-
planVersionId: '
|
|
1284
|
+
planVersionId: 'free-v1',
|
|
1282
1285
|
versionNumber: 1,
|
|
1283
1286
|
seatLimit: null,
|
|
1284
1287
|
metadata: {}
|
|
@@ -1292,9 +1295,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1292
1295
|
return Promise.resolve({
|
|
1293
1296
|
ok: true,
|
|
1294
1297
|
json: () => Promise.resolve({
|
|
1295
|
-
planId: '
|
|
1296
|
-
planName: '
|
|
1297
|
-
planVersionId: '
|
|
1298
|
+
planId: 'free',
|
|
1299
|
+
planName: 'Free',
|
|
1300
|
+
planVersionId: 'free-v1',
|
|
1298
1301
|
entitlementState: 'active'
|
|
1299
1302
|
})
|
|
1300
1303
|
});
|
|
@@ -1323,7 +1326,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1323
1326
|
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
|
|
1324
1327
|
await waitFor(() => {
|
|
1325
1328
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1326
|
-
expect(screen.getByText('
|
|
1329
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1327
1330
|
});
|
|
1328
1331
|
await waitFor(() => {
|
|
1329
1332
|
expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
|
|
@@ -1393,8 +1396,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1393
1396
|
return Promise.resolve({
|
|
1394
1397
|
ok: true,
|
|
1395
1398
|
json: () => Promise.resolve({
|
|
1396
|
-
planId: '
|
|
1397
|
-
planName: '
|
|
1399
|
+
planId: 'free',
|
|
1400
|
+
planName: 'Free',
|
|
1398
1401
|
entitlementState: 'active'
|
|
1399
1402
|
})
|
|
1400
1403
|
});
|
|
@@ -1450,14 +1453,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
1450
1453
|
});
|
|
1451
1454
|
await waitFor(() => {
|
|
1452
1455
|
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1453
|
-
expect(screen.getByText('
|
|
1456
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1454
1457
|
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1455
1458
|
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1456
1459
|
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
1457
1460
|
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1458
1461
|
expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
|
|
1459
1462
|
});
|
|
1460
|
-
await user.click(screen.getByRole('button', { name: /Subscription
|
|
1463
|
+
await user.click(screen.getByRole('button', { name: /Subscription Free/i }));
|
|
1461
1464
|
await waitFor(() => {
|
|
1462
1465
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1463
1466
|
});
|
|
@@ -1512,8 +1515,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1512
1515
|
return Promise.resolve({
|
|
1513
1516
|
ok: true,
|
|
1514
1517
|
json: () => Promise.resolve({
|
|
1515
|
-
planId: '
|
|
1516
|
-
planName: '
|
|
1518
|
+
planId: 'free',
|
|
1519
|
+
planName: 'Free',
|
|
1517
1520
|
entitlementState: 'active'
|
|
1518
1521
|
})
|
|
1519
1522
|
});
|
|
@@ -1563,7 +1566,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1563
1566
|
}
|
|
1564
1567
|
await waitFor(() => {
|
|
1565
1568
|
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1566
|
-
expect(screen.getByText('
|
|
1569
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1567
1570
|
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1568
1571
|
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1569
1572
|
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
@@ -1590,9 +1593,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1590
1593
|
userId: 'user-new',
|
|
1591
1594
|
displayName: 'New User',
|
|
1592
1595
|
workspaceId: 'workspace-local-active',
|
|
1593
|
-
planId: '
|
|
1594
|
-
planVersionId: '
|
|
1595
|
-
planName: '
|
|
1596
|
+
planId: 'free',
|
|
1597
|
+
planVersionId: 'free-v1',
|
|
1598
|
+
planName: 'Free',
|
|
1596
1599
|
teamManagementAllowed: false,
|
|
1597
1600
|
teamPlanMode: 'personal',
|
|
1598
1601
|
workspaceRole: 'member'
|
|
@@ -1690,11 +1693,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
1690
1693
|
return jsonResponse({
|
|
1691
1694
|
plans: [
|
|
1692
1695
|
{
|
|
1693
|
-
planId: '
|
|
1694
|
-
displayName: '
|
|
1696
|
+
planId: 'free',
|
|
1697
|
+
displayName: 'Free',
|
|
1695
1698
|
status: 'active',
|
|
1696
1699
|
defaultVersion: {
|
|
1697
|
-
planVersionId: '
|
|
1700
|
+
planVersionId: 'free-v1',
|
|
1698
1701
|
versionNumber: 1,
|
|
1699
1702
|
seatLimit: null,
|
|
1700
1703
|
metadata: {}
|
|
@@ -1825,7 +1828,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1825
1828
|
await user.click(screen.getByTitle(/Account/i));
|
|
1826
1829
|
await waitFor(() => {
|
|
1827
1830
|
expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
|
|
1828
|
-
expect(screen.getByText('
|
|
1831
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1829
1832
|
});
|
|
1830
1833
|
await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
|
|
1831
1834
|
await waitFor(() => {
|
|
@@ -1846,7 +1849,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1846
1849
|
await waitFor(() => {
|
|
1847
1850
|
expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
|
|
1848
1851
|
expect(screen.getByText('Team 5')).toBeInTheDocument();
|
|
1849
|
-
expect(screen.queryByText('
|
|
1852
|
+
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
|
1850
1853
|
expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
|
|
1851
1854
|
});
|
|
1852
1855
|
await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
|
|
@@ -1897,16 +1900,144 @@ describe('TaskforceCore Integration', () => {
|
|
|
1897
1900
|
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1898
1901
|
await waitFor(() => {
|
|
1899
1902
|
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
1903
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
1904
|
+
expect(screen.getByLabelText('Display name')).toBeInTheDocument();
|
|
1900
1905
|
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1901
1906
|
expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
|
|
1902
1907
|
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
1903
1908
|
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
1904
1909
|
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1905
1910
|
expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
|
|
1911
|
+
expect(screen.getByTestId('auth-submit-button')).toBeInTheDocument();
|
|
1912
|
+
expect(document.title).toBe('Create Account | Taskforce');
|
|
1906
1913
|
});
|
|
1907
1914
|
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
1908
1915
|
});
|
|
1909
|
-
it('
|
|
1916
|
+
it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
|
|
1917
|
+
const fetchMock = vi.fn((url) => {
|
|
1918
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1919
|
+
return Promise.resolve({
|
|
1920
|
+
ok: true,
|
|
1921
|
+
json: () => Promise.resolve({
|
|
1922
|
+
authenticated: false,
|
|
1923
|
+
runtimeMode: 'cloud',
|
|
1924
|
+
authRequiredForApi: false
|
|
1925
|
+
})
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1929
|
+
return Promise.resolve({
|
|
1930
|
+
ok: true,
|
|
1931
|
+
json: () => Promise.resolve({
|
|
1932
|
+
...mockConfig,
|
|
1933
|
+
runtimeMode: 'cloud',
|
|
1934
|
+
authRequiredForApi: false,
|
|
1935
|
+
setupState: {
|
|
1936
|
+
globalSetupState: 'present',
|
|
1937
|
+
workspaceSetupState: 'present',
|
|
1938
|
+
runtimeMode: 'cloud',
|
|
1939
|
+
workspaceId: 'workspace-local-active',
|
|
1940
|
+
mode: 'core',
|
|
1941
|
+
forceSetup: false,
|
|
1942
|
+
forceGlobalSetup: false,
|
|
1943
|
+
forceWorkspaceSetup: false
|
|
1944
|
+
}
|
|
1945
|
+
})
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1949
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1950
|
+
}
|
|
1951
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1952
|
+
});
|
|
1953
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1954
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
1955
|
+
await waitFor(() => {
|
|
1956
|
+
expect(screen.getByText('Enter your verification token.')).toBeInTheDocument();
|
|
1957
|
+
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1958
|
+
expect(screen.getByRole('button', { name: 'Verify Email' })).toBeInTheDocument();
|
|
1959
|
+
expect(screen.getByRole('button', { name: 'Back to Sign In' })).toBeInTheDocument();
|
|
1960
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1961
|
+
expect(screen.getByRole('button', { name: 'Resend Verification' })).toBeDisabled();
|
|
1962
|
+
});
|
|
1963
|
+
});
|
|
1964
|
+
it('allows the verify screen to resend a verification email after the user re-enters their email', async () => {
|
|
1965
|
+
const user = userEvent.setup();
|
|
1966
|
+
const fetchMock = vi.fn((input) => {
|
|
1967
|
+
const url = String(input);
|
|
1968
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1969
|
+
return Promise.resolve({
|
|
1970
|
+
ok: true,
|
|
1971
|
+
json: () => Promise.resolve({
|
|
1972
|
+
authenticated: false,
|
|
1973
|
+
runtimeMode: 'cloud',
|
|
1974
|
+
authRequiredForApi: false
|
|
1975
|
+
})
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1979
|
+
return Promise.resolve({
|
|
1980
|
+
ok: true,
|
|
1981
|
+
json: () => Promise.resolve({
|
|
1982
|
+
...mockConfig,
|
|
1983
|
+
runtimeMode: 'cloud',
|
|
1984
|
+
authRequiredForApi: false,
|
|
1985
|
+
setupState: {
|
|
1986
|
+
globalSetupState: 'present',
|
|
1987
|
+
workspaceSetupState: 'present',
|
|
1988
|
+
runtimeMode: 'cloud',
|
|
1989
|
+
workspaceId: 'workspace-local-active',
|
|
1990
|
+
mode: 'core',
|
|
1991
|
+
forceSetup: false,
|
|
1992
|
+
forceGlobalSetup: false,
|
|
1993
|
+
forceWorkspaceSetup: false
|
|
1994
|
+
}
|
|
1995
|
+
})
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1999
|
+
return Promise.resolve({
|
|
2000
|
+
ok: true,
|
|
2001
|
+
json: () => Promise.resolve({
|
|
2002
|
+
success: true,
|
|
2003
|
+
verificationRequired: true,
|
|
2004
|
+
workspaceSetupRequired: false,
|
|
2005
|
+
emailSent: true
|
|
2006
|
+
})
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (url.includes('/api/taskforce/auth/verify-email/request')) {
|
|
2010
|
+
return Promise.resolve({
|
|
2011
|
+
ok: true,
|
|
2012
|
+
json: () => Promise.resolve({
|
|
2013
|
+
success: true,
|
|
2014
|
+
verificationToken: 'resent-token'
|
|
2015
|
+
})
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2019
|
+
return Promise.resolve({
|
|
2020
|
+
ok: true,
|
|
2021
|
+
json: () => Promise.resolve({
|
|
2022
|
+
plans: []
|
|
2023
|
+
})
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2027
|
+
});
|
|
2028
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2029
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
2030
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
|
|
2031
|
+
await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
|
|
2032
|
+
await waitFor(() => {
|
|
2033
|
+
expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeInTheDocument();
|
|
2034
|
+
expect(screen.getByDisplayValue('resent-token')).toBeInTheDocument();
|
|
2035
|
+
expect(screen.getByText('Verification email resent.')).toBeInTheDocument();
|
|
2036
|
+
});
|
|
2037
|
+
expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
|
|
2038
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
|
|
2039
|
+
});
|
|
2040
|
+
it('shows neutral verification copy when resend succeeds without attempting delivery', async () => {
|
|
1910
2041
|
const user = userEvent.setup();
|
|
1911
2042
|
const fetchMock = vi.fn((input) => {
|
|
1912
2043
|
const url = String(input);
|
|
@@ -1940,6 +2071,113 @@ describe('TaskforceCore Integration', () => {
|
|
|
1940
2071
|
})
|
|
1941
2072
|
});
|
|
1942
2073
|
}
|
|
2074
|
+
if (url.includes('/api/taskforce/auth/verify-email/request')) {
|
|
2075
|
+
return Promise.resolve({
|
|
2076
|
+
ok: true,
|
|
2077
|
+
json: () => Promise.resolve({
|
|
2078
|
+
success: true,
|
|
2079
|
+
emailSent: false,
|
|
2080
|
+
deliveryAttempted: false,
|
|
2081
|
+
verificationToken: null
|
|
2082
|
+
})
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2086
|
+
return Promise.resolve({
|
|
2087
|
+
ok: true,
|
|
2088
|
+
json: () => Promise.resolve({
|
|
2089
|
+
plans: []
|
|
2090
|
+
})
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2094
|
+
});
|
|
2095
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2096
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
2097
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
|
|
2098
|
+
await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
|
|
2099
|
+
await waitFor(() => {
|
|
2100
|
+
expect(screen.getByText('If that account exists and still needs verification, we sent an email.')).toBeInTheDocument();
|
|
2101
|
+
});
|
|
2102
|
+
});
|
|
2103
|
+
it('does not switch to verify mode after registration when verification is not required', async () => {
|
|
2104
|
+
const user = userEvent.setup();
|
|
2105
|
+
let authenticated = false;
|
|
2106
|
+
const fetchMock = vi.fn((input) => {
|
|
2107
|
+
const url = String(input);
|
|
2108
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2109
|
+
return Promise.resolve({
|
|
2110
|
+
ok: true,
|
|
2111
|
+
json: () => Promise.resolve({
|
|
2112
|
+
config: {
|
|
2113
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2114
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2115
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2116
|
+
runtimeMode: 'cloud',
|
|
2117
|
+
authSource: 'cloud',
|
|
2118
|
+
workspaceMode: 'multi-cloud',
|
|
2119
|
+
workspaceSwitchingEnabled: true,
|
|
2120
|
+
authRequiredForApi: true
|
|
2121
|
+
}
|
|
2122
|
+
})
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2126
|
+
return Promise.resolve({
|
|
2127
|
+
ok: true,
|
|
2128
|
+
json: () => Promise.resolve({
|
|
2129
|
+
authenticated,
|
|
2130
|
+
runtimeMode: 'cloud',
|
|
2131
|
+
authRequiredForApi: true,
|
|
2132
|
+
userId: authenticated ? 'user-no-verify' : null,
|
|
2133
|
+
workspaceId: null,
|
|
2134
|
+
betaAccess: true
|
|
2135
|
+
})
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2139
|
+
return Promise.resolve({
|
|
2140
|
+
ok: true,
|
|
2141
|
+
json: () => Promise.resolve({
|
|
2142
|
+
...mockConfig,
|
|
2143
|
+
runtimeMode: 'cloud',
|
|
2144
|
+
authRequiredForApi: true,
|
|
2145
|
+
workspaceSwitchingEnabled: true,
|
|
2146
|
+
setupState: {
|
|
2147
|
+
globalSetupState: 'present',
|
|
2148
|
+
workspaceSetupState: 'missing',
|
|
2149
|
+
runtimeMode: 'cloud',
|
|
2150
|
+
workspaceId: 'default',
|
|
2151
|
+
mode: 'core',
|
|
2152
|
+
forceSetup: true,
|
|
2153
|
+
forceGlobalSetup: false,
|
|
2154
|
+
forceWorkspaceSetup: true
|
|
2155
|
+
}
|
|
2156
|
+
})
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2160
|
+
return Promise.resolve({
|
|
2161
|
+
ok: true,
|
|
2162
|
+
json: () => Promise.resolve({
|
|
2163
|
+
success: true,
|
|
2164
|
+
workspaces: [],
|
|
2165
|
+
currentWorkspaceId: 'default'
|
|
2166
|
+
})
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2170
|
+
return Promise.resolve({
|
|
2171
|
+
ok: true,
|
|
2172
|
+
json: () => Promise.resolve({
|
|
2173
|
+
gate: 'ok',
|
|
2174
|
+
hasEntitlement: true,
|
|
2175
|
+
canAccessApp: true,
|
|
2176
|
+
canAccessPlans: true,
|
|
2177
|
+
message: null
|
|
2178
|
+
})
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
1943
2181
|
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1944
2182
|
return Promise.resolve({
|
|
1945
2183
|
ok: true,
|
|
@@ -1949,12 +2187,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
1949
2187
|
});
|
|
1950
2188
|
}
|
|
1951
2189
|
if (url.includes('/api/taskforce/auth/register')) {
|
|
2190
|
+
authenticated = true;
|
|
1952
2191
|
return Promise.resolve({
|
|
1953
2192
|
ok: true,
|
|
1954
2193
|
json: () => Promise.resolve({
|
|
1955
2194
|
success: true,
|
|
1956
|
-
workspaceSetupRequired:
|
|
1957
|
-
verificationRequired: false
|
|
2195
|
+
workspaceSetupRequired: true,
|
|
2196
|
+
verificationRequired: false,
|
|
2197
|
+
commercialState: 'active'
|
|
1958
2198
|
})
|
|
1959
2199
|
});
|
|
1960
2200
|
}
|
|
@@ -1967,7 +2207,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1967
2207
|
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1968
2208
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1969
2209
|
await waitFor(() => {
|
|
1970
|
-
expect(screen.getByText('
|
|
2210
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
1971
2211
|
});
|
|
1972
2212
|
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
1973
2213
|
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
@@ -2054,11 +2294,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
2054
2294
|
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2055
2295
|
return jsonResponse({
|
|
2056
2296
|
plans: [{
|
|
2057
|
-
planId: '
|
|
2058
|
-
displayName: '
|
|
2297
|
+
planId: 'free',
|
|
2298
|
+
displayName: 'Free',
|
|
2059
2299
|
status: 'active',
|
|
2060
2300
|
defaultVersion: {
|
|
2061
|
-
planVersionId: '
|
|
2301
|
+
planVersionId: 'free-v1',
|
|
2062
2302
|
versionNumber: 1,
|
|
2063
2303
|
seatLimit: null,
|
|
2064
2304
|
metadata: {}
|
|
@@ -2066,10 +2306,10 @@ describe('TaskforceCore Integration', () => {
|
|
|
2066
2306
|
features: [],
|
|
2067
2307
|
pricing: {
|
|
2068
2308
|
month: {
|
|
2069
|
-
planVersionId: '
|
|
2309
|
+
planVersionId: 'free-v1',
|
|
2070
2310
|
interval: 'month',
|
|
2071
2311
|
pricingType: 'free',
|
|
2072
|
-
stripePriceId: 'free:
|
|
2312
|
+
stripePriceId: 'free:free-v1:month',
|
|
2073
2313
|
displayAmount: '$0.00',
|
|
2074
2314
|
unitAmount: 0,
|
|
2075
2315
|
currency: 'usd'
|
|
@@ -2130,7 +2370,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2130
2370
|
});
|
|
2131
2371
|
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
|
|
2132
2372
|
});
|
|
2133
|
-
it('
|
|
2373
|
+
it('keeps registration on plan selection when commercial onboarding is blocked even if the new cloud user has no workspace yet', async () => {
|
|
2134
2374
|
const user = userEvent.setup();
|
|
2135
2375
|
let authenticated = false;
|
|
2136
2376
|
const fetchMock = vi.fn((input) => {
|
|
@@ -2154,8 +2394,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
2154
2394
|
authenticated,
|
|
2155
2395
|
runtimeMode: 'cloud',
|
|
2156
2396
|
authRequiredForApi: true,
|
|
2157
|
-
userId: authenticated ? 'user-
|
|
2158
|
-
workspaceId:
|
|
2397
|
+
userId: authenticated ? 'user-new' : null,
|
|
2398
|
+
workspaceId: null,
|
|
2159
2399
|
betaAccess: true
|
|
2160
2400
|
});
|
|
2161
2401
|
}
|
|
@@ -2167,9 +2407,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
2167
2407
|
workspaceSwitchingEnabled: true,
|
|
2168
2408
|
setupState: {
|
|
2169
2409
|
globalSetupState: 'present',
|
|
2170
|
-
workspaceSetupState: '
|
|
2410
|
+
workspaceSetupState: 'missing',
|
|
2171
2411
|
runtimeMode: 'cloud',
|
|
2172
|
-
workspaceId: '
|
|
2412
|
+
workspaceId: 'default',
|
|
2173
2413
|
mode: 'core',
|
|
2174
2414
|
forceSetup: false,
|
|
2175
2415
|
forceGlobalSetup: false,
|
|
@@ -2180,67 +2420,183 @@ describe('TaskforceCore Integration', () => {
|
|
|
2180
2420
|
if (url.includes('/api/taskforce/workspaces')) {
|
|
2181
2421
|
return jsonResponse({
|
|
2182
2422
|
success: true,
|
|
2183
|
-
workspaces: [
|
|
2184
|
-
|
|
2185
|
-
slug: 'workspace-1',
|
|
2186
|
-
name: 'Workspace 1',
|
|
2187
|
-
description: null,
|
|
2188
|
-
role: 'owner',
|
|
2189
|
-
status: 'active',
|
|
2190
|
-
betaAccess: true
|
|
2191
|
-
}],
|
|
2192
|
-
currentWorkspaceId: 'workspace-1'
|
|
2423
|
+
workspaces: [],
|
|
2424
|
+
currentWorkspaceId: 'default'
|
|
2193
2425
|
});
|
|
2194
2426
|
}
|
|
2195
2427
|
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2196
2428
|
return jsonResponse({
|
|
2197
|
-
gate: '
|
|
2429
|
+
gate: 'plan_selection_required',
|
|
2198
2430
|
hasEntitlement: true,
|
|
2199
2431
|
canAccessApp: false,
|
|
2200
2432
|
canAccessPlans: true,
|
|
2201
|
-
message: '
|
|
2433
|
+
message: 'Choose a plan to continue.'
|
|
2202
2434
|
});
|
|
2203
2435
|
}
|
|
2204
2436
|
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2205
2437
|
return jsonResponse({
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
gate: 'checkout_pending',
|
|
2211
|
-
message: 'Complete checkout to continue.',
|
|
2212
|
-
workspaceId: 'workspace-1'
|
|
2438
|
+
entitlementState: 'pending_plan_selection',
|
|
2439
|
+
gate: 'plan_selection_required',
|
|
2440
|
+
message: 'Choose a plan to continue.',
|
|
2441
|
+
workspaceId: null
|
|
2213
2442
|
});
|
|
2214
2443
|
}
|
|
2215
2444
|
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2445
|
+
return jsonResponse({ plans: [] });
|
|
2446
|
+
}
|
|
2447
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2448
|
+
authenticated = true;
|
|
2216
2449
|
return jsonResponse({
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2450
|
+
success: true,
|
|
2451
|
+
workspaceSetupRequired: true,
|
|
2452
|
+
verificationRequired: false,
|
|
2453
|
+
planSelectionRequired: true,
|
|
2454
|
+
commercialState: 'pending_plan_selection'
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2458
|
+
return jsonResponse(mockCategories);
|
|
2459
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2460
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2461
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2462
|
+
return jsonResponse({ archived: [] });
|
|
2463
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2464
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2465
|
+
if (url.includes('/api/taskforce/types'))
|
|
2466
|
+
return jsonResponse({ types: [] });
|
|
2467
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2468
|
+
return jsonResponse({ priorities: [] });
|
|
2469
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2470
|
+
return jsonResponse({ approaches: [] });
|
|
2471
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2472
|
+
return jsonResponse({ specialists: [] });
|
|
2473
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2474
|
+
return jsonResponse({ templates: [] });
|
|
2475
|
+
return jsonResponse({});
|
|
2476
|
+
});
|
|
2477
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2478
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2479
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
|
|
2480
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
|
|
2481
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2482
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2483
|
+
await waitFor(() => {
|
|
2484
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2485
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
2486
|
+
});
|
|
2487
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2488
|
+
});
|
|
2489
|
+
it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
|
|
2490
|
+
const user = userEvent.setup();
|
|
2491
|
+
let authenticated = false;
|
|
2492
|
+
const fetchMock = vi.fn((input) => {
|
|
2493
|
+
const url = String(input);
|
|
2494
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2495
|
+
return jsonResponse({
|
|
2496
|
+
config: {
|
|
2497
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2498
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2499
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2500
|
+
runtimeMode: 'cloud',
|
|
2501
|
+
authSource: 'cloud',
|
|
2502
|
+
workspaceMode: 'multi-cloud',
|
|
2503
|
+
workspaceSwitchingEnabled: true,
|
|
2504
|
+
authRequiredForApi: true
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2509
|
+
return jsonResponse({
|
|
2510
|
+
authenticated,
|
|
2511
|
+
runtimeMode: 'cloud',
|
|
2512
|
+
authRequiredForApi: true,
|
|
2513
|
+
userId: authenticated ? 'user-1' : null,
|
|
2514
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2515
|
+
betaAccess: true
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2519
|
+
return jsonResponse({
|
|
2520
|
+
...mockConfig,
|
|
2521
|
+
runtimeMode: 'cloud',
|
|
2522
|
+
authRequiredForApi: true,
|
|
2523
|
+
workspaceSwitchingEnabled: true,
|
|
2524
|
+
setupState: {
|
|
2525
|
+
globalSetupState: 'present',
|
|
2526
|
+
workspaceSetupState: 'present',
|
|
2527
|
+
runtimeMode: 'cloud',
|
|
2528
|
+
workspaceId: 'workspace-1',
|
|
2529
|
+
mode: 'core',
|
|
2530
|
+
forceSetup: false,
|
|
2531
|
+
forceGlobalSetup: false,
|
|
2532
|
+
forceWorkspaceSetup: false
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2537
|
+
return jsonResponse({
|
|
2538
|
+
success: true,
|
|
2539
|
+
workspaces: [{
|
|
2540
|
+
id: 'workspace-1',
|
|
2541
|
+
slug: 'workspace-1',
|
|
2542
|
+
name: 'Workspace 1',
|
|
2543
|
+
description: null,
|
|
2544
|
+
role: 'owner',
|
|
2545
|
+
status: 'active',
|
|
2546
|
+
betaAccess: true
|
|
2547
|
+
}],
|
|
2548
|
+
currentWorkspaceId: 'workspace-1'
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2552
|
+
return jsonResponse({
|
|
2553
|
+
gate: 'checkout_pending',
|
|
2554
|
+
hasEntitlement: true,
|
|
2555
|
+
canAccessApp: false,
|
|
2556
|
+
canAccessPlans: true,
|
|
2557
|
+
message: 'Complete checkout to continue.'
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2561
|
+
return jsonResponse({
|
|
2562
|
+
planId: 'basic',
|
|
2563
|
+
planName: 'Basic',
|
|
2564
|
+
planVersionId: 'basic-v1',
|
|
2565
|
+
entitlementState: 'checkout_pending',
|
|
2566
|
+
gate: 'checkout_pending',
|
|
2567
|
+
message: 'Complete checkout to continue.',
|
|
2568
|
+
workspaceId: 'workspace-1'
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2572
|
+
return jsonResponse({
|
|
2573
|
+
plans: [{
|
|
2574
|
+
planId: 'basic',
|
|
2575
|
+
displayName: 'Basic',
|
|
2576
|
+
status: 'active',
|
|
2577
|
+
defaultVersion: {
|
|
2578
|
+
planVersionId: 'basic-v1',
|
|
2579
|
+
versionNumber: 1,
|
|
2580
|
+
seatLimit: null,
|
|
2581
|
+
metadata: {}
|
|
2582
|
+
},
|
|
2583
|
+
features: [],
|
|
2584
|
+
pricing: {
|
|
2585
|
+
month: {
|
|
2586
|
+
planVersionId: 'basic-v1',
|
|
2587
|
+
interval: 'month',
|
|
2588
|
+
pricingType: 'stripe',
|
|
2589
|
+
stripePriceId: 'price_basic_month',
|
|
2590
|
+
displayAmount: '$19.00',
|
|
2591
|
+
unitAmount: 1900,
|
|
2592
|
+
currency: 'usd'
|
|
2593
|
+
},
|
|
2594
|
+
year: {
|
|
2595
|
+
planVersionId: 'basic-v1',
|
|
2596
|
+
interval: 'year',
|
|
2597
|
+
pricingType: 'stripe',
|
|
2598
|
+
stripePriceId: 'price_basic_year',
|
|
2599
|
+
displayAmount: '$190.00',
|
|
2244
2600
|
unitAmount: 19000,
|
|
2245
2601
|
currency: 'usd'
|
|
2246
2602
|
}
|
|
@@ -2294,7 +2650,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2294
2650
|
return jsonResponse({});
|
|
2295
2651
|
});
|
|
2296
2652
|
vi.stubGlobal('fetch', fetchMock);
|
|
2297
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=
|
|
2653
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=basic&planVersionId=basic-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2298
2654
|
await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
|
|
2299
2655
|
await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
|
|
2300
2656
|
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
@@ -2306,69 +2662,330 @@ describe('TaskforceCore Integration', () => {
|
|
|
2306
2662
|
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
|
|
2307
2663
|
});
|
|
2308
2664
|
});
|
|
2309
|
-
it('
|
|
2665
|
+
it('routes successful registration into workspace setup when onboarding auto-grants free, even if a stale pre-registration session check resolves later', async () => {
|
|
2310
2666
|
const user = userEvent.setup();
|
|
2667
|
+
let authenticated = false;
|
|
2668
|
+
let authSessionCallCount = 0;
|
|
2669
|
+
let releaseInitialSession = null;
|
|
2670
|
+
const initialSessionGate = new Promise((resolve) => {
|
|
2671
|
+
releaseInitialSession = resolve;
|
|
2672
|
+
});
|
|
2311
2673
|
const fetchMock = vi.fn((input) => {
|
|
2312
2674
|
const url = String(input);
|
|
2313
|
-
if (url.includes('/api/taskforce/auth/
|
|
2314
|
-
return
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2675
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2676
|
+
return jsonResponse({
|
|
2677
|
+
config: {
|
|
2678
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2679
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2680
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2318
2681
|
runtimeMode: 'cloud',
|
|
2682
|
+
authSource: 'cloud',
|
|
2683
|
+
workspaceMode: 'multi-cloud',
|
|
2684
|
+
workspaceSwitchingEnabled: true,
|
|
2319
2685
|
authRequiredForApi: true
|
|
2320
|
-
}
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2690
|
+
authSessionCallCount += 1;
|
|
2691
|
+
const payload = () => ({
|
|
2692
|
+
authenticated,
|
|
2693
|
+
runtimeMode: 'cloud',
|
|
2694
|
+
authRequiredForApi: true,
|
|
2695
|
+
userId: authenticated ? 'user-free-1' : null,
|
|
2696
|
+
workspaceId: null,
|
|
2697
|
+
betaAccess: true
|
|
2321
2698
|
});
|
|
2699
|
+
if (authSessionCallCount === 1) {
|
|
2700
|
+
return initialSessionGate.then(() => jsonResponse(payload()));
|
|
2701
|
+
}
|
|
2702
|
+
return jsonResponse(payload());
|
|
2322
2703
|
}
|
|
2323
2704
|
if (url.includes('/api/taskforce/config')) {
|
|
2324
|
-
return
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2705
|
+
return jsonResponse({
|
|
2706
|
+
...mockConfig,
|
|
2707
|
+
runtimeMode: 'cloud',
|
|
2708
|
+
authRequiredForApi: true,
|
|
2709
|
+
workspaceSwitchingEnabled: true,
|
|
2710
|
+
setupState: {
|
|
2711
|
+
globalSetupState: 'present',
|
|
2712
|
+
workspaceSetupState: 'missing',
|
|
2328
2713
|
runtimeMode: 'cloud',
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
mode: 'core',
|
|
2336
|
-
forceSetup: false,
|
|
2337
|
-
forceGlobalSetup: false,
|
|
2338
|
-
forceWorkspaceSetup: false
|
|
2339
|
-
}
|
|
2340
|
-
})
|
|
2714
|
+
workspaceId: 'default',
|
|
2715
|
+
mode: 'core',
|
|
2716
|
+
forceSetup: true,
|
|
2717
|
+
forceGlobalSetup: false,
|
|
2718
|
+
forceWorkspaceSetup: true
|
|
2719
|
+
}
|
|
2341
2720
|
});
|
|
2342
2721
|
}
|
|
2343
|
-
if (url.includes('/api/taskforce/
|
|
2344
|
-
return
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2722
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2723
|
+
return jsonResponse({
|
|
2724
|
+
success: true,
|
|
2725
|
+
workspaces: [],
|
|
2726
|
+
currentWorkspaceId: 'default'
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2730
|
+
return jsonResponse({
|
|
2731
|
+
gate: 'ok',
|
|
2732
|
+
hasEntitlement: true,
|
|
2733
|
+
canAccessApp: true,
|
|
2734
|
+
canAccessPlans: true,
|
|
2735
|
+
message: null
|
|
2349
2736
|
});
|
|
2350
2737
|
}
|
|
2738
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2739
|
+
return jsonResponse({ plans: [] });
|
|
2740
|
+
}
|
|
2351
2741
|
if (url.includes('/api/taskforce/auth/register')) {
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
emailSent: true
|
|
2359
|
-
})
|
|
2742
|
+
authenticated = true;
|
|
2743
|
+
return jsonResponse({
|
|
2744
|
+
success: true,
|
|
2745
|
+
workspaceSetupRequired: true,
|
|
2746
|
+
verificationRequired: false,
|
|
2747
|
+
commercialState: 'active'
|
|
2360
2748
|
});
|
|
2361
2749
|
}
|
|
2362
|
-
|
|
2750
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2751
|
+
return jsonResponse(mockCategories);
|
|
2752
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2753
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2754
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2755
|
+
return jsonResponse({ archived: [] });
|
|
2756
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2757
|
+
return jsonResponse({ dataVersion: 1 });
|
|
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/approaches'))
|
|
2763
|
+
return jsonResponse({ approaches: [] });
|
|
2764
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2765
|
+
return jsonResponse({ specialists: [] });
|
|
2766
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2767
|
+
return jsonResponse({ templates: [] });
|
|
2768
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
2769
|
+
return jsonResponse({
|
|
2770
|
+
packs: [{
|
|
2771
|
+
id: 'general',
|
|
2772
|
+
label: 'General',
|
|
2773
|
+
description: 'Starter',
|
|
2774
|
+
isDefaultStarter: true,
|
|
2775
|
+
sections: {
|
|
2776
|
+
categories: [{ value: 'general', label: 'General' }],
|
|
2777
|
+
taskTypes: [{ value: 'task', label: 'Task' }],
|
|
2778
|
+
priorities: [{ value: 1, label: 'Low' }]
|
|
2779
|
+
}
|
|
2780
|
+
}]
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
return jsonResponse({});
|
|
2363
2784
|
});
|
|
2364
2785
|
vi.stubGlobal('fetch', fetchMock);
|
|
2365
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%
|
|
2366
|
-
await user.type(await screen.findByPlaceholderText('Email'), '
|
|
2367
|
-
await user.type(screen.getByPlaceholderText('Display name'), '
|
|
2786
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2787
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
|
|
2788
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
|
|
2368
2789
|
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2369
2790
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2791
|
+
await act(async () => {
|
|
2792
|
+
await Promise.resolve();
|
|
2793
|
+
});
|
|
2370
2794
|
await waitFor(() => {
|
|
2371
|
-
expect(screen.
|
|
2795
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2796
|
+
});
|
|
2797
|
+
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/taskforce/auth/session')).length).toBeGreaterThanOrEqual(2);
|
|
2798
|
+
await act(async () => {
|
|
2799
|
+
releaseInitialSession?.();
|
|
2800
|
+
await Promise.resolve();
|
|
2801
|
+
});
|
|
2802
|
+
await waitFor(() => {
|
|
2803
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2804
|
+
});
|
|
2805
|
+
expect(screen.queryByText('Sign in with your account credentials.')).not.toBeInTheDocument();
|
|
2806
|
+
});
|
|
2807
|
+
it('routes successful registration directly into the app when onboarding auto-grants free and the new cloud user already has an accessible workspace', async () => {
|
|
2808
|
+
const user = userEvent.setup();
|
|
2809
|
+
let authenticated = false;
|
|
2810
|
+
const fetchMock = vi.fn((input) => {
|
|
2811
|
+
const url = String(input);
|
|
2812
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2813
|
+
return jsonResponse({
|
|
2814
|
+
config: {
|
|
2815
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2816
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2817
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2818
|
+
runtimeMode: 'cloud',
|
|
2819
|
+
authSource: 'cloud',
|
|
2820
|
+
workspaceMode: 'multi-cloud',
|
|
2821
|
+
workspaceSwitchingEnabled: true,
|
|
2822
|
+
authRequiredForApi: true
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2827
|
+
return jsonResponse({
|
|
2828
|
+
authenticated,
|
|
2829
|
+
runtimeMode: 'cloud',
|
|
2830
|
+
authRequiredForApi: true,
|
|
2831
|
+
userId: authenticated ? 'user-free-2' : null,
|
|
2832
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2833
|
+
betaAccess: true
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2837
|
+
return jsonResponse({
|
|
2838
|
+
...mockConfig,
|
|
2839
|
+
runtimeMode: 'cloud',
|
|
2840
|
+
authRequiredForApi: true,
|
|
2841
|
+
workspaceSwitchingEnabled: true,
|
|
2842
|
+
workspaceId: 'workspace-1',
|
|
2843
|
+
setupState: {
|
|
2844
|
+
globalSetupState: 'present',
|
|
2845
|
+
workspaceSetupState: 'present',
|
|
2846
|
+
runtimeMode: 'cloud',
|
|
2847
|
+
workspaceId: 'workspace-1',
|
|
2848
|
+
mode: 'core',
|
|
2849
|
+
forceSetup: false,
|
|
2850
|
+
forceGlobalSetup: false,
|
|
2851
|
+
forceWorkspaceSetup: false
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2856
|
+
return jsonResponse({
|
|
2857
|
+
success: true,
|
|
2858
|
+
workspaces: [{
|
|
2859
|
+
id: 'workspace-1',
|
|
2860
|
+
slug: 'workspace-1',
|
|
2861
|
+
name: 'Workspace 1',
|
|
2862
|
+
description: null,
|
|
2863
|
+
role: 'owner',
|
|
2864
|
+
status: 'active',
|
|
2865
|
+
betaAccess: true
|
|
2866
|
+
}],
|
|
2867
|
+
currentWorkspaceId: 'workspace-1'
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2871
|
+
return jsonResponse({
|
|
2872
|
+
gate: 'ok',
|
|
2873
|
+
hasEntitlement: true,
|
|
2874
|
+
canAccessApp: true,
|
|
2875
|
+
canAccessPlans: true,
|
|
2876
|
+
message: null
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2880
|
+
return jsonResponse({ plans: [] });
|
|
2881
|
+
}
|
|
2882
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2883
|
+
authenticated = true;
|
|
2884
|
+
return jsonResponse({
|
|
2885
|
+
success: true,
|
|
2886
|
+
workspaceSetupRequired: false,
|
|
2887
|
+
verificationRequired: false,
|
|
2888
|
+
commercialState: 'active'
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2892
|
+
return jsonResponse(mockCategories);
|
|
2893
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2894
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2895
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2896
|
+
return jsonResponse({ archived: [] });
|
|
2897
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2898
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2899
|
+
if (url.includes('/api/taskforce/types'))
|
|
2900
|
+
return jsonResponse({ types: [] });
|
|
2901
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2902
|
+
return jsonResponse({ priorities: [] });
|
|
2903
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2904
|
+
return jsonResponse({ approaches: [] });
|
|
2905
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2906
|
+
return jsonResponse({ specialists: [] });
|
|
2907
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2908
|
+
return jsonResponse({ templates: [] });
|
|
2909
|
+
return jsonResponse({});
|
|
2910
|
+
});
|
|
2911
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2912
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2913
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
|
|
2914
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
|
|
2915
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2916
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2917
|
+
await act(async () => {
|
|
2918
|
+
await Promise.resolve();
|
|
2919
|
+
});
|
|
2920
|
+
await waitFor(() => {
|
|
2921
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2922
|
+
});
|
|
2923
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2924
|
+
expect(screen.queryByRole('heading', { name: 'Subscription Plans' })).not.toBeInTheDocument();
|
|
2925
|
+
});
|
|
2926
|
+
it('shows verification before workspace setup when registration still requires email verification', async () => {
|
|
2927
|
+
const user = userEvent.setup();
|
|
2928
|
+
const fetchMock = vi.fn((input) => {
|
|
2929
|
+
const url = String(input);
|
|
2930
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2931
|
+
return Promise.resolve({
|
|
2932
|
+
ok: true,
|
|
2933
|
+
json: () => Promise.resolve({
|
|
2934
|
+
authenticated: false,
|
|
2935
|
+
runtimeMode: 'cloud',
|
|
2936
|
+
authRequiredForApi: true
|
|
2937
|
+
})
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2941
|
+
return Promise.resolve({
|
|
2942
|
+
ok: true,
|
|
2943
|
+
json: () => Promise.resolve({
|
|
2944
|
+
...mockConfig,
|
|
2945
|
+
runtimeMode: 'cloud',
|
|
2946
|
+
authRequiredForApi: true,
|
|
2947
|
+
setupState: {
|
|
2948
|
+
globalSetupState: 'present',
|
|
2949
|
+
workspaceSetupState: 'present',
|
|
2950
|
+
runtimeMode: 'cloud',
|
|
2951
|
+
workspaceId: 'default',
|
|
2952
|
+
mode: 'core',
|
|
2953
|
+
forceSetup: false,
|
|
2954
|
+
forceGlobalSetup: false,
|
|
2955
|
+
forceWorkspaceSetup: false
|
|
2956
|
+
}
|
|
2957
|
+
})
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2961
|
+
return Promise.resolve({
|
|
2962
|
+
ok: true,
|
|
2963
|
+
json: () => Promise.resolve({
|
|
2964
|
+
plans: []
|
|
2965
|
+
})
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2969
|
+
return Promise.resolve({
|
|
2970
|
+
ok: true,
|
|
2971
|
+
json: () => Promise.resolve({
|
|
2972
|
+
success: true,
|
|
2973
|
+
workspaceSetupRequired: true,
|
|
2974
|
+
verificationRequired: true,
|
|
2975
|
+
emailSent: true
|
|
2976
|
+
})
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2980
|
+
});
|
|
2981
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2982
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
2983
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
2984
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
2985
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2986
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2987
|
+
await waitFor(() => {
|
|
2988
|
+
expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
|
|
2372
2989
|
});
|
|
2373
2990
|
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
2374
2991
|
expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
|
|
@@ -2426,7 +3043,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2426
3043
|
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
2427
3044
|
});
|
|
2428
3045
|
});
|
|
2429
|
-
it('
|
|
3046
|
+
it('shows neutral password reset copy when reset delivery was not attempted', async () => {
|
|
2430
3047
|
const user = userEvent.setup();
|
|
2431
3048
|
const fetchMock = vi.fn((input) => {
|
|
2432
3049
|
const url = String(input);
|
|
@@ -2454,7 +3071,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2454
3071
|
json: () => Promise.resolve({
|
|
2455
3072
|
authenticated: false,
|
|
2456
3073
|
runtimeMode: 'cloud',
|
|
2457
|
-
authRequiredForApi:
|
|
3074
|
+
authRequiredForApi: false
|
|
2458
3075
|
})
|
|
2459
3076
|
});
|
|
2460
3077
|
}
|
|
@@ -2464,7 +3081,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2464
3081
|
json: () => Promise.resolve({
|
|
2465
3082
|
...mockConfig,
|
|
2466
3083
|
runtimeMode: 'cloud',
|
|
2467
|
-
authRequiredForApi:
|
|
3084
|
+
authRequiredForApi: false,
|
|
2468
3085
|
setupState: {
|
|
2469
3086
|
globalSetupState: 'present',
|
|
2470
3087
|
workspaceSetupState: 'present',
|
|
@@ -2478,62 +3095,59 @@ describe('TaskforceCore Integration', () => {
|
|
|
2478
3095
|
})
|
|
2479
3096
|
});
|
|
2480
3097
|
}
|
|
2481
|
-
if (url.includes('/api/taskforce/
|
|
2482
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2483
|
-
}
|
|
2484
|
-
if (url.includes('/api/taskforce/public/pricing')) {
|
|
3098
|
+
if (url.includes('/api/taskforce/auth/password-reset/request')) {
|
|
2485
3099
|
return Promise.resolve({
|
|
2486
3100
|
ok: true,
|
|
2487
3101
|
json: () => Promise.resolve({
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
displayName: 'Free',
|
|
2493
|
-
status: 'active',
|
|
2494
|
-
defaultVersion: { planVersionId: 'free-v1', versionNumber: 1, seatLimit: 1, metadata: {} },
|
|
2495
|
-
features: [],
|
|
2496
|
-
pricing: {
|
|
2497
|
-
month: { planVersionId: 'free-v1', interval: 'month', pricingType: 'free', stripePriceId: 'free:free-v1:month', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' },
|
|
2498
|
-
year: { planVersionId: 'free-v1', interval: 'year', pricingType: 'free', stripePriceId: 'free:free-v1:year', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' }
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
]
|
|
3102
|
+
success: true,
|
|
3103
|
+
emailSent: false,
|
|
3104
|
+
deliveryAttempted: false,
|
|
3105
|
+
resetToken: null
|
|
2502
3106
|
})
|
|
2503
3107
|
});
|
|
2504
3108
|
}
|
|
2505
|
-
if (url.includes('/api/taskforce/
|
|
2506
|
-
return Promise.resolve({
|
|
2507
|
-
ok: false,
|
|
2508
|
-
json: () => Promise.resolve({
|
|
2509
|
-
success: false,
|
|
2510
|
-
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
2511
|
-
error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
|
|
2512
|
-
})
|
|
2513
|
-
});
|
|
3109
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3110
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2514
3111
|
}
|
|
2515
3112
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2516
3113
|
});
|
|
2517
3114
|
vi.stubGlobal('fetch', fetchMock);
|
|
2518
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=
|
|
2519
|
-
await user.type(await screen.findByPlaceholderText('Email'), '
|
|
2520
|
-
await user.
|
|
2521
|
-
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2522
|
-
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
3115
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3116
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'nobody@example.com');
|
|
3117
|
+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
|
|
2523
3118
|
await waitFor(() => {
|
|
2524
|
-
expect(screen.getByText('
|
|
2525
|
-
expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
|
|
3119
|
+
expect(screen.getByText('If that account exists, we sent password reset instructions.')).toBeInTheDocument();
|
|
2526
3120
|
});
|
|
2527
3121
|
});
|
|
2528
|
-
it('
|
|
2529
|
-
const
|
|
3122
|
+
it('keeps forgot-password users on the request form when reset delivery fails', async () => {
|
|
3123
|
+
const user = userEvent.setup();
|
|
3124
|
+
const fetchMock = vi.fn((input) => {
|
|
3125
|
+
const url = String(input);
|
|
3126
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3127
|
+
return Promise.resolve({
|
|
3128
|
+
ok: true,
|
|
3129
|
+
json: () => Promise.resolve({
|
|
3130
|
+
success: true,
|
|
3131
|
+
config: {
|
|
3132
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3133
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3134
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3135
|
+
runtimeMode: 'cloud',
|
|
3136
|
+
authSource: 'cloud',
|
|
3137
|
+
workspaceMode: 'multi-cloud',
|
|
3138
|
+
workspaceSwitchingEnabled: true,
|
|
3139
|
+
cloudAuthViaLocalProxy: false
|
|
3140
|
+
}
|
|
3141
|
+
})
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2530
3144
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2531
3145
|
return Promise.resolve({
|
|
2532
3146
|
ok: true,
|
|
2533
3147
|
json: () => Promise.resolve({
|
|
2534
3148
|
authenticated: false,
|
|
2535
3149
|
runtimeMode: 'cloud',
|
|
2536
|
-
authRequiredForApi:
|
|
3150
|
+
authRequiredForApi: false
|
|
2537
3151
|
})
|
|
2538
3152
|
});
|
|
2539
3153
|
}
|
|
@@ -2543,12 +3157,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
2543
3157
|
json: () => Promise.resolve({
|
|
2544
3158
|
...mockConfig,
|
|
2545
3159
|
runtimeMode: 'cloud',
|
|
2546
|
-
authRequiredForApi:
|
|
3160
|
+
authRequiredForApi: false,
|
|
2547
3161
|
setupState: {
|
|
2548
3162
|
globalSetupState: 'present',
|
|
2549
3163
|
workspaceSetupState: 'present',
|
|
2550
3164
|
runtimeMode: 'cloud',
|
|
2551
|
-
workspaceId: 'workspace-
|
|
3165
|
+
workspaceId: 'workspace-local-active',
|
|
2552
3166
|
mode: 'core',
|
|
2553
3167
|
forceSetup: false,
|
|
2554
3168
|
forceGlobalSetup: false,
|
|
@@ -2557,16 +3171,15 @@ describe('TaskforceCore Integration', () => {
|
|
|
2557
3171
|
})
|
|
2558
3172
|
});
|
|
2559
3173
|
}
|
|
2560
|
-
if (url.includes('/api/taskforce/auth/
|
|
3174
|
+
if (url.includes('/api/taskforce/auth/password-reset/request')) {
|
|
2561
3175
|
return Promise.resolve({
|
|
2562
3176
|
ok: true,
|
|
2563
3177
|
json: () => Promise.resolve({
|
|
2564
3178
|
success: true,
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
passwordRequired: false
|
|
3179
|
+
emailSent: false,
|
|
3180
|
+
deliveryAttempted: true,
|
|
3181
|
+
emailError: 'SMTP temporary failure',
|
|
3182
|
+
resetToken: null
|
|
2570
3183
|
})
|
|
2571
3184
|
});
|
|
2572
3185
|
}
|
|
@@ -2576,16 +3189,38 @@ describe('TaskforceCore Integration', () => {
|
|
|
2576
3189
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2577
3190
|
});
|
|
2578
3191
|
vi.stubGlobal('fetch', fetchMock);
|
|
2579
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=
|
|
3192
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3193
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'owner@example.com');
|
|
3194
|
+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
|
|
2580
3195
|
await waitFor(() => {
|
|
2581
|
-
expect(screen.getByText('
|
|
2582
|
-
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
2583
|
-
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
2584
|
-
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
3196
|
+
expect(screen.getByText('Password reset email failed to send. SMTP temporary failure')).toBeInTheDocument();
|
|
2585
3197
|
});
|
|
3198
|
+
expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
|
|
3199
|
+
expect(screen.queryByText('Enter the password reset token and choose a new password.')).not.toBeInTheDocument();
|
|
3200
|
+
expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
|
|
2586
3201
|
});
|
|
2587
|
-
it('
|
|
2588
|
-
const
|
|
3202
|
+
it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
|
|
3203
|
+
const user = userEvent.setup();
|
|
3204
|
+
const fetchMock = vi.fn((input) => {
|
|
3205
|
+
const url = String(input);
|
|
3206
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3207
|
+
return Promise.resolve({
|
|
3208
|
+
ok: true,
|
|
3209
|
+
json: () => Promise.resolve({
|
|
3210
|
+
success: true,
|
|
3211
|
+
config: {
|
|
3212
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3213
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3214
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3215
|
+
runtimeMode: 'cloud',
|
|
3216
|
+
authSource: 'cloud',
|
|
3217
|
+
workspaceMode: 'multi-cloud',
|
|
3218
|
+
workspaceSwitchingEnabled: true,
|
|
3219
|
+
cloudAuthViaLocalProxy: false
|
|
3220
|
+
}
|
|
3221
|
+
})
|
|
3222
|
+
});
|
|
3223
|
+
}
|
|
2589
3224
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2590
3225
|
return Promise.resolve({
|
|
2591
3226
|
ok: true,
|
|
@@ -2607,7 +3242,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2607
3242
|
globalSetupState: 'present',
|
|
2608
3243
|
workspaceSetupState: 'present',
|
|
2609
3244
|
runtimeMode: 'cloud',
|
|
2610
|
-
workspaceId: 'workspace-
|
|
3245
|
+
workspaceId: 'workspace-local-active',
|
|
2611
3246
|
mode: 'core',
|
|
2612
3247
|
forceSetup: false,
|
|
2613
3248
|
forceGlobalSetup: false,
|
|
@@ -2616,80 +3251,97 @@ describe('TaskforceCore Integration', () => {
|
|
|
2616
3251
|
})
|
|
2617
3252
|
});
|
|
2618
3253
|
}
|
|
2619
|
-
if (url.includes('/api/taskforce/
|
|
3254
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3255
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3256
|
+
}
|
|
3257
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2620
3258
|
return Promise.resolve({
|
|
2621
3259
|
ok: true,
|
|
2622
3260
|
json: () => Promise.resolve({
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3261
|
+
asOf: '2026-03-16T00:00:00.000Z',
|
|
3262
|
+
plans: [
|
|
3263
|
+
{
|
|
3264
|
+
planId: 'free',
|
|
3265
|
+
displayName: 'Free',
|
|
3266
|
+
status: 'active',
|
|
3267
|
+
defaultVersion: { planVersionId: 'free-v1', versionNumber: 1, seatLimit: 1, metadata: {} },
|
|
3268
|
+
features: [],
|
|
3269
|
+
pricing: {
|
|
3270
|
+
month: { planVersionId: 'free-v1', interval: 'month', pricingType: 'free', stripePriceId: 'free:free-v1:month', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' },
|
|
3271
|
+
year: { planVersionId: 'free-v1', interval: 'year', pricingType: 'free', stripePriceId: 'free:free-v1:year', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' }
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
]
|
|
2629
3275
|
})
|
|
2630
3276
|
});
|
|
2631
3277
|
}
|
|
2632
|
-
if (url.includes('/api/taskforce/
|
|
2633
|
-
return Promise.resolve({
|
|
3278
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
3279
|
+
return Promise.resolve({
|
|
3280
|
+
ok: false,
|
|
3281
|
+
json: () => Promise.resolve({
|
|
3282
|
+
success: false,
|
|
3283
|
+
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
3284
|
+
error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
|
|
3285
|
+
})
|
|
3286
|
+
});
|
|
2634
3287
|
}
|
|
2635
3288
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2636
3289
|
});
|
|
2637
3290
|
vi.stubGlobal('fetch', fetchMock);
|
|
2638
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=
|
|
3291
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3292
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
3293
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
3294
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
3295
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2639
3296
|
await waitFor(() => {
|
|
2640
|
-
expect(screen.getByText('
|
|
2641
|
-
expect(screen.
|
|
2642
|
-
expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
|
|
3297
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
3298
|
+
expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
|
|
2643
3299
|
});
|
|
2644
3300
|
});
|
|
2645
|
-
it('shows
|
|
2646
|
-
const
|
|
2647
|
-
let selectedMode = null;
|
|
2648
|
-
const fetchMock = vi.fn((url, init) => {
|
|
3301
|
+
it('shows sign-in continuation for existing-user invites', async () => {
|
|
3302
|
+
const fetchMock = vi.fn((url) => {
|
|
2649
3303
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2650
3304
|
return Promise.resolve({
|
|
2651
3305
|
ok: true,
|
|
2652
3306
|
json: () => Promise.resolve({
|
|
2653
|
-
authenticated:
|
|
2654
|
-
runtimeMode: '
|
|
2655
|
-
authRequiredForApi:
|
|
3307
|
+
authenticated: false,
|
|
3308
|
+
runtimeMode: 'cloud',
|
|
3309
|
+
authRequiredForApi: true
|
|
2656
3310
|
})
|
|
2657
3311
|
});
|
|
2658
3312
|
}
|
|
2659
3313
|
if (url.includes('/api/taskforce/config')) {
|
|
2660
|
-
const mode = selectedMode;
|
|
2661
3314
|
return Promise.resolve({
|
|
2662
3315
|
ok: true,
|
|
2663
3316
|
json: () => Promise.resolve({
|
|
2664
3317
|
...mockConfig,
|
|
2665
|
-
runtimeMode: '
|
|
2666
|
-
authRequiredForApi:
|
|
3318
|
+
runtimeMode: 'cloud',
|
|
3319
|
+
authRequiredForApi: true,
|
|
2667
3320
|
setupState: {
|
|
2668
|
-
globalSetupState:
|
|
3321
|
+
globalSetupState: 'present',
|
|
2669
3322
|
workspaceSetupState: 'present',
|
|
2670
|
-
runtimeMode: '
|
|
2671
|
-
workspaceId: '
|
|
2672
|
-
mode:
|
|
3323
|
+
runtimeMode: 'cloud',
|
|
3324
|
+
workspaceId: 'workspace-1',
|
|
3325
|
+
mode: 'core',
|
|
2673
3326
|
forceSetup: false,
|
|
2674
|
-
forceGlobalSetup:
|
|
3327
|
+
forceGlobalSetup: false,
|
|
2675
3328
|
forceWorkspaceSetup: false
|
|
2676
|
-
},
|
|
2677
|
-
runtimeCapabilities: {
|
|
2678
|
-
runtimeMode: 'local',
|
|
2679
|
-
supportsCloudAuth: false,
|
|
2680
|
-
supportsAdminManagedSetupOverrides: false,
|
|
2681
|
-
supportsPackageScriptSetupOverrides: true
|
|
2682
3329
|
}
|
|
2683
3330
|
})
|
|
2684
3331
|
});
|
|
2685
3332
|
}
|
|
2686
|
-
if (url.includes('/api/taskforce/
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3333
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3334
|
+
return Promise.resolve({
|
|
3335
|
+
ok: true,
|
|
3336
|
+
json: () => Promise.resolve({
|
|
3337
|
+
success: true,
|
|
3338
|
+
state: 'valid',
|
|
3339
|
+
email: 'existing@example.com',
|
|
3340
|
+
workspaceId: 'workspace-1',
|
|
3341
|
+
inviteeKind: 'existing_user',
|
|
3342
|
+
passwordRequired: false
|
|
3343
|
+
})
|
|
3344
|
+
});
|
|
2693
3345
|
}
|
|
2694
3346
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2695
3347
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
@@ -2697,28 +3349,28 @@ describe('TaskforceCore Integration', () => {
|
|
|
2697
3349
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2698
3350
|
});
|
|
2699
3351
|
vi.stubGlobal('fetch', fetchMock);
|
|
2700
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2701
|
-
await waitFor(() => {
|
|
2702
|
-
expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
|
|
2703
|
-
});
|
|
2704
|
-
await user.click(screen.getByRole('radio', { name: /operations mode/i }));
|
|
2705
|
-
await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
|
|
3352
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2706
3353
|
await waitFor(() => {
|
|
2707
|
-
expect(screen.
|
|
3354
|
+
expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
|
|
3355
|
+
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
3356
|
+
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
3357
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
2708
3358
|
});
|
|
2709
|
-
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
|
|
2710
3359
|
});
|
|
2711
|
-
it('
|
|
2712
|
-
|
|
2713
|
-
let
|
|
2714
|
-
const fetchMock = vi.fn((url
|
|
3360
|
+
it('uses invite join after invite-mode sign-in even if invite reinspection has not refreshed yet', async () => {
|
|
3361
|
+
let authenticated = false;
|
|
3362
|
+
let inviteInspectCalls = 0;
|
|
3363
|
+
const fetchMock = vi.fn((url) => {
|
|
2715
3364
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2716
3365
|
return Promise.resolve({
|
|
2717
3366
|
ok: true,
|
|
2718
3367
|
json: () => Promise.resolve({
|
|
2719
|
-
authenticated
|
|
2720
|
-
runtimeMode: '
|
|
2721
|
-
authRequiredForApi:
|
|
3368
|
+
authenticated,
|
|
3369
|
+
runtimeMode: 'cloud',
|
|
3370
|
+
authRequiredForApi: true,
|
|
3371
|
+
userId: authenticated ? 'user-existing' : 'anonymous',
|
|
3372
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3373
|
+
email: authenticated ? 'existing@example.com' : null
|
|
2722
3374
|
})
|
|
2723
3375
|
});
|
|
2724
3376
|
}
|
|
@@ -2727,37 +3379,92 @@ describe('TaskforceCore Integration', () => {
|
|
|
2727
3379
|
ok: true,
|
|
2728
3380
|
json: () => Promise.resolve({
|
|
2729
3381
|
...mockConfig,
|
|
2730
|
-
runtimeMode: '
|
|
2731
|
-
authRequiredForApi:
|
|
3382
|
+
runtimeMode: 'cloud',
|
|
3383
|
+
authRequiredForApi: true,
|
|
2732
3384
|
setupState: {
|
|
2733
3385
|
globalSetupState: 'present',
|
|
2734
|
-
workspaceSetupState:
|
|
2735
|
-
runtimeMode: '
|
|
2736
|
-
workspaceId: 'default',
|
|
3386
|
+
workspaceSetupState: 'present',
|
|
3387
|
+
runtimeMode: 'cloud',
|
|
3388
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
2737
3389
|
mode: 'core',
|
|
2738
3390
|
forceSetup: false,
|
|
2739
3391
|
forceGlobalSetup: false,
|
|
2740
|
-
forceWorkspaceSetup:
|
|
2741
|
-
workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
|
|
2742
|
-
suggestedWorkspaceName: 'Suggested Project'
|
|
2743
|
-
},
|
|
2744
|
-
runtimeCapabilities: {
|
|
2745
|
-
runtimeMode: 'local',
|
|
2746
|
-
supportsCloudAuth: false,
|
|
2747
|
-
supportsAdminManagedSetupOverrides: false,
|
|
2748
|
-
supportsPackageScriptSetupOverrides: true
|
|
3392
|
+
forceWorkspaceSetup: false
|
|
2749
3393
|
}
|
|
2750
3394
|
})
|
|
2751
3395
|
});
|
|
2752
3396
|
}
|
|
2753
|
-
if (url.includes('/api/taskforce/
|
|
2754
|
-
|
|
2755
|
-
if (
|
|
2756
|
-
|
|
2757
|
-
|
|
3397
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3398
|
+
inviteInspectCalls += 1;
|
|
3399
|
+
if (authenticated && inviteInspectCalls > 1) {
|
|
3400
|
+
return new Promise(() => undefined);
|
|
3401
|
+
}
|
|
3402
|
+
return Promise.resolve({
|
|
3403
|
+
ok: true,
|
|
3404
|
+
json: () => Promise.resolve({
|
|
3405
|
+
success: true,
|
|
3406
|
+
state: 'valid',
|
|
3407
|
+
email: 'existing@example.com',
|
|
3408
|
+
workspaceId: 'workspace-1',
|
|
3409
|
+
inviteeKind: 'existing_user',
|
|
3410
|
+
passwordRequired: false
|
|
3411
|
+
})
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
3415
|
+
authenticated = true;
|
|
3416
|
+
return Promise.resolve({
|
|
3417
|
+
ok: true,
|
|
3418
|
+
json: () => Promise.resolve({ success: true })
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
if (url.includes('/api/taskforce/auth/invite/join')) {
|
|
3422
|
+
return Promise.resolve({
|
|
3423
|
+
ok: true,
|
|
3424
|
+
json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' })
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
3428
|
+
return Promise.resolve({
|
|
3429
|
+
ok: true,
|
|
3430
|
+
json: () => Promise.resolve({
|
|
3431
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3432
|
+
workspaces: authenticated
|
|
3433
|
+
? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
|
|
3434
|
+
: []
|
|
3435
|
+
})
|
|
3436
|
+
});
|
|
2758
3437
|
}
|
|
2759
3438
|
if (url.includes('/api/taskforce/tasks')) {
|
|
2760
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks:
|
|
3439
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
|
|
3440
|
+
}
|
|
3441
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3442
|
+
return Promise.resolve({
|
|
3443
|
+
ok: true,
|
|
3444
|
+
json: () => Promise.resolve({
|
|
3445
|
+
success: true,
|
|
3446
|
+
authenticated,
|
|
3447
|
+
commercialState: 'active',
|
|
3448
|
+
planSelectionRequired: false,
|
|
3449
|
+
checkoutPending: false
|
|
3450
|
+
})
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
3454
|
+
return Promise.resolve({
|
|
3455
|
+
ok: true,
|
|
3456
|
+
json: () => Promise.resolve({
|
|
3457
|
+
success: true,
|
|
3458
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3459
|
+
workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
|
|
3460
|
+
})
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3464
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' }) });
|
|
3465
|
+
}
|
|
3466
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3467
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
|
|
2761
3468
|
}
|
|
2762
3469
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2763
3470
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
@@ -2765,160 +3472,1132 @@ describe('TaskforceCore Integration', () => {
|
|
|
2765
3472
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2766
3473
|
});
|
|
2767
3474
|
vi.stubGlobal('fetch', fetchMock);
|
|
2768
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
3475
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2769
3476
|
await waitFor(() => {
|
|
2770
|
-
expect(screen.
|
|
3477
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
2771
3478
|
});
|
|
2772
|
-
|
|
2773
|
-
await
|
|
2774
|
-
await user.type(nameInput, 'Delta Project');
|
|
2775
|
-
await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
|
|
2776
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
3479
|
+
await userEvent.type(screen.getByPlaceholderText('Password'), 'ExistingUserPass123!');
|
|
3480
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
2777
3481
|
await waitFor(() => {
|
|
2778
|
-
expect(screen.
|
|
3482
|
+
expect(screen.getByText('Signed in. Review the invite details to continue.')).toBeInTheDocument();
|
|
3483
|
+
expect(screen.getByRole('button', { name: 'Join Workspace' })).toBeInTheDocument();
|
|
2779
3484
|
});
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
return false;
|
|
2784
|
-
const init = call[1];
|
|
2785
|
-
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
2786
|
-
return false;
|
|
2787
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
2788
|
-
return body?.stateKey === 'workspace-sync';
|
|
3485
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
3486
|
+
await waitFor(() => {
|
|
3487
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(true);
|
|
2789
3488
|
});
|
|
2790
|
-
|
|
2791
|
-
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
2792
|
-
expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
|
|
3489
|
+
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
|
|
2793
3490
|
});
|
|
2794
|
-
it('
|
|
2795
|
-
|
|
2796
|
-
let
|
|
3491
|
+
it('keeps existing-user invites recoverable when the wrong account signs in first', async () => {
|
|
3492
|
+
let authenticated = false;
|
|
3493
|
+
let sessionEmail = null;
|
|
2797
3494
|
const fetchMock = vi.fn((url, init) => {
|
|
2798
3495
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2799
|
-
return
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
3496
|
+
return Promise.resolve({
|
|
3497
|
+
ok: true,
|
|
3498
|
+
json: () => Promise.resolve({
|
|
3499
|
+
authenticated,
|
|
3500
|
+
runtimeMode: 'cloud',
|
|
3501
|
+
authRequiredForApi: true,
|
|
3502
|
+
userId: authenticated ? 'user-other' : 'anonymous',
|
|
3503
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3504
|
+
email: sessionEmail
|
|
3505
|
+
})
|
|
2803
3506
|
});
|
|
2804
3507
|
}
|
|
2805
3508
|
if (url.includes('/api/taskforce/config')) {
|
|
2806
|
-
return
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3509
|
+
return Promise.resolve({
|
|
3510
|
+
ok: true,
|
|
3511
|
+
json: () => Promise.resolve({
|
|
3512
|
+
...mockConfig,
|
|
3513
|
+
runtimeMode: 'cloud',
|
|
3514
|
+
authRequiredForApi: true,
|
|
3515
|
+
setupState: {
|
|
3516
|
+
globalSetupState: 'present',
|
|
3517
|
+
workspaceSetupState: 'present',
|
|
3518
|
+
runtimeMode: 'cloud',
|
|
3519
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3520
|
+
mode: 'core',
|
|
3521
|
+
forceSetup: false,
|
|
3522
|
+
forceGlobalSetup: false,
|
|
3523
|
+
forceWorkspaceSetup: false
|
|
3524
|
+
}
|
|
3525
|
+
})
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3529
|
+
return Promise.resolve({
|
|
3530
|
+
ok: true,
|
|
3531
|
+
json: () => Promise.resolve({
|
|
3532
|
+
success: true,
|
|
3533
|
+
state: 'valid',
|
|
3534
|
+
email: 'existing@example.com',
|
|
3535
|
+
workspaceId: 'workspace-1',
|
|
3536
|
+
inviteeKind: 'existing_user',
|
|
3537
|
+
passwordRequired: false
|
|
3538
|
+
})
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
3542
|
+
const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
|
|
3543
|
+
authenticated = true;
|
|
3544
|
+
sessionEmail = String(body?.email || '').trim().toLowerCase() || null;
|
|
3545
|
+
return Promise.resolve({
|
|
3546
|
+
ok: true,
|
|
3547
|
+
json: () => Promise.resolve({ success: true })
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
if (url.includes('/api/taskforce/auth/invite/join')) {
|
|
3551
|
+
return Promise.resolve({
|
|
3552
|
+
ok: false,
|
|
3553
|
+
json: () => Promise.resolve({
|
|
3554
|
+
success: false,
|
|
3555
|
+
code: 'INVITE_ACCOUNT_MISMATCH',
|
|
3556
|
+
error: 'Sign in with the invited account to join this workspace.'
|
|
3557
|
+
})
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
3561
|
+
return Promise.resolve({
|
|
3562
|
+
ok: true,
|
|
3563
|
+
json: () => Promise.resolve({
|
|
3564
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3565
|
+
workspaces: authenticated
|
|
3566
|
+
? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
|
|
3567
|
+
: []
|
|
3568
|
+
})
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3572
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
|
|
3573
|
+
}
|
|
3574
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3575
|
+
return Promise.resolve({
|
|
3576
|
+
ok: true,
|
|
3577
|
+
json: () => Promise.resolve({
|
|
3578
|
+
success: true,
|
|
3579
|
+
authenticated,
|
|
3580
|
+
commercialState: 'active',
|
|
3581
|
+
planSelectionRequired: false,
|
|
3582
|
+
checkoutPending: false
|
|
3583
|
+
})
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
3587
|
+
return Promise.resolve({
|
|
3588
|
+
ok: true,
|
|
3589
|
+
json: () => Promise.resolve({
|
|
3590
|
+
success: true,
|
|
3591
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3592
|
+
workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
|
|
3593
|
+
})
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3597
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-home' }) });
|
|
3598
|
+
}
|
|
3599
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3600
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
|
|
3601
|
+
}
|
|
3602
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3603
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3604
|
+
}
|
|
3605
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3606
|
+
});
|
|
3607
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3608
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3609
|
+
await waitFor(() => {
|
|
3610
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
3611
|
+
});
|
|
3612
|
+
await userEvent.clear(screen.getByPlaceholderText('Email'));
|
|
3613
|
+
await userEvent.type(screen.getByPlaceholderText('Email'), 'other@example.com');
|
|
3614
|
+
await userEvent.type(screen.getByPlaceholderText('Password'), 'OtherUserPass123!');
|
|
3615
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
3616
|
+
await waitFor(() => {
|
|
3617
|
+
expect(screen.getByText('Sign in with the invited account to join this workspace.')).toBeInTheDocument();
|
|
3618
|
+
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
3619
|
+
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
3620
|
+
expect(screen.queryByRole('button', { name: 'Join Workspace' })).not.toBeInTheDocument();
|
|
3621
|
+
});
|
|
3622
|
+
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
|
|
3623
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(false);
|
|
3624
|
+
});
|
|
3625
|
+
it('shows password creation for new-user invites', async () => {
|
|
3626
|
+
const fetchMock = vi.fn((url) => {
|
|
3627
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3628
|
+
return Promise.resolve({
|
|
3629
|
+
ok: true,
|
|
3630
|
+
json: () => Promise.resolve({
|
|
3631
|
+
authenticated: false,
|
|
3632
|
+
runtimeMode: 'cloud',
|
|
3633
|
+
authRequiredForApi: true
|
|
3634
|
+
})
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3638
|
+
return Promise.resolve({
|
|
3639
|
+
ok: true,
|
|
3640
|
+
json: () => Promise.resolve({
|
|
3641
|
+
...mockConfig,
|
|
3642
|
+
runtimeMode: 'cloud',
|
|
3643
|
+
authRequiredForApi: true,
|
|
3644
|
+
setupState: {
|
|
3645
|
+
globalSetupState: 'present',
|
|
3646
|
+
workspaceSetupState: 'present',
|
|
3647
|
+
runtimeMode: 'cloud',
|
|
3648
|
+
workspaceId: 'workspace-1',
|
|
3649
|
+
mode: 'core',
|
|
3650
|
+
forceSetup: false,
|
|
3651
|
+
forceGlobalSetup: false,
|
|
3652
|
+
forceWorkspaceSetup: false
|
|
3653
|
+
}
|
|
3654
|
+
})
|
|
3655
|
+
});
|
|
3656
|
+
}
|
|
3657
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3658
|
+
return Promise.resolve({
|
|
3659
|
+
ok: true,
|
|
3660
|
+
json: () => Promise.resolve({
|
|
3661
|
+
success: true,
|
|
3662
|
+
state: 'valid',
|
|
3663
|
+
email: 'new@example.com',
|
|
3664
|
+
workspaceId: 'workspace-1',
|
|
3665
|
+
inviteeKind: 'new_user',
|
|
3666
|
+
passwordRequired: true
|
|
3667
|
+
})
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3671
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3672
|
+
}
|
|
3673
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3674
|
+
});
|
|
3675
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3676
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3677
|
+
await waitFor(() => {
|
|
3678
|
+
expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
|
|
3679
|
+
expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
|
|
3680
|
+
expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
|
|
3681
|
+
});
|
|
3682
|
+
});
|
|
3683
|
+
it('shows global setup route when forced and persists selected mode', async () => {
|
|
3684
|
+
const user = userEvent.setup();
|
|
3685
|
+
let selectedMode = null;
|
|
3686
|
+
const fetchMock = vi.fn((url, init) => {
|
|
3687
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3688
|
+
return Promise.resolve({
|
|
3689
|
+
ok: true,
|
|
3690
|
+
json: () => Promise.resolve({
|
|
3691
|
+
authenticated: true,
|
|
2825
3692
|
runtimeMode: 'local',
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
supportsPackageScriptSetupOverrides: true
|
|
2829
|
-
}
|
|
3693
|
+
authRequiredForApi: false
|
|
3694
|
+
})
|
|
2830
3695
|
});
|
|
2831
3696
|
}
|
|
2832
|
-
if (url.includes('/api/taskforce/
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
3697
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3698
|
+
const mode = selectedMode;
|
|
3699
|
+
return Promise.resolve({
|
|
3700
|
+
ok: true,
|
|
3701
|
+
json: () => Promise.resolve({
|
|
3702
|
+
...mockConfig,
|
|
3703
|
+
runtimeMode: 'local',
|
|
3704
|
+
authRequiredForApi: false,
|
|
3705
|
+
setupState: {
|
|
3706
|
+
globalSetupState: mode ? 'present' : 'missing',
|
|
3707
|
+
workspaceSetupState: 'present',
|
|
3708
|
+
runtimeMode: 'local',
|
|
3709
|
+
workspaceId: 'default',
|
|
3710
|
+
mode: mode || 'core',
|
|
3711
|
+
forceSetup: false,
|
|
3712
|
+
forceGlobalSetup: true,
|
|
3713
|
+
forceWorkspaceSetup: false
|
|
2843
3714
|
},
|
|
2844
|
-
{
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
types: [
|
|
2850
|
-
{ value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500' },
|
|
2851
|
-
{ value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500' }
|
|
2852
|
-
],
|
|
2853
|
-
priorities: [
|
|
2854
|
-
{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' },
|
|
2855
|
-
{ value: 2, label: 'Medium', icon: 'Minus', color: 'yellow-500' },
|
|
2856
|
-
{ value: 3, label: 'High', icon: 'ArrowUp', color: 'orange-500' },
|
|
2857
|
-
{ value: 4, label: 'Critical', icon: 'AlertTriangle', color: 'red-500' }
|
|
2858
|
-
]
|
|
3715
|
+
runtimeCapabilities: {
|
|
3716
|
+
runtimeMode: 'local',
|
|
3717
|
+
supportsCloudAuth: false,
|
|
3718
|
+
supportsAdminManagedSetupOverrides: false,
|
|
3719
|
+
supportsPackageScriptSetupOverrides: true
|
|
2859
3720
|
}
|
|
2860
|
-
|
|
3721
|
+
})
|
|
2861
3722
|
});
|
|
2862
3723
|
}
|
|
2863
|
-
if (url.includes('/api/taskforce/
|
|
3724
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
2864
3725
|
const body = JSON.parse(String(init?.body || '{}'));
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3726
|
+
selectedMode = body?.setup?.mode === 'operations' ? 'operations' : 'core';
|
|
3727
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3728
|
+
}
|
|
3729
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3730
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3731
|
+
}
|
|
3732
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3733
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3734
|
+
}
|
|
3735
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3736
|
+
});
|
|
3737
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3738
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
3739
|
+
await waitFor(() => {
|
|
3740
|
+
expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
|
|
3741
|
+
});
|
|
3742
|
+
await user.click(screen.getByRole('radio', { name: /operations mode/i }));
|
|
3743
|
+
await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
|
|
3744
|
+
await waitFor(() => {
|
|
3745
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3746
|
+
});
|
|
3747
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
|
|
3748
|
+
});
|
|
3749
|
+
it('shows workspace setup route when required and persists workspace profile', async () => {
|
|
3750
|
+
const user = userEvent.setup();
|
|
3751
|
+
let workspaceSaved = false;
|
|
3752
|
+
const fetchMock = vi.fn((url, init) => {
|
|
3753
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3754
|
+
return Promise.resolve({
|
|
3755
|
+
ok: true,
|
|
3756
|
+
json: () => Promise.resolve({
|
|
3757
|
+
authenticated: true,
|
|
3758
|
+
runtimeMode: 'local',
|
|
3759
|
+
authRequiredForApi: false
|
|
3760
|
+
})
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3764
|
+
return Promise.resolve({
|
|
3765
|
+
ok: true,
|
|
3766
|
+
json: () => Promise.resolve({
|
|
3767
|
+
...mockConfig,
|
|
3768
|
+
runtimeMode: 'local',
|
|
3769
|
+
authRequiredForApi: false,
|
|
3770
|
+
setupState: {
|
|
3771
|
+
globalSetupState: 'present',
|
|
3772
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
3773
|
+
runtimeMode: 'local',
|
|
3774
|
+
workspaceId: 'default',
|
|
3775
|
+
mode: 'core',
|
|
3776
|
+
forceSetup: false,
|
|
3777
|
+
forceGlobalSetup: false,
|
|
3778
|
+
forceWorkspaceSetup: true,
|
|
3779
|
+
workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
|
|
3780
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
3781
|
+
},
|
|
3782
|
+
runtimeCapabilities: {
|
|
3783
|
+
runtimeMode: 'local',
|
|
3784
|
+
supportsCloudAuth: false,
|
|
3785
|
+
supportsAdminManagedSetupOverrides: false,
|
|
3786
|
+
supportsPackageScriptSetupOverrides: true
|
|
3787
|
+
}
|
|
3788
|
+
})
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
3792
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
3793
|
+
if (body?.name === 'Delta Project')
|
|
3794
|
+
workspaceSaved = true;
|
|
3795
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3796
|
+
}
|
|
3797
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3798
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3799
|
+
}
|
|
3800
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3801
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3802
|
+
}
|
|
3803
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3804
|
+
});
|
|
3805
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3806
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
3807
|
+
await waitFor(() => {
|
|
3808
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3809
|
+
});
|
|
3810
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
3811
|
+
await user.clear(nameInput);
|
|
3812
|
+
await user.type(nameInput, 'Delta Project');
|
|
3813
|
+
await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Workspace desc');
|
|
3814
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3815
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
3816
|
+
await waitFor(() => {
|
|
3817
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3818
|
+
});
|
|
3819
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
|
|
3820
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
3821
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
3822
|
+
return false;
|
|
3823
|
+
const init = call[1];
|
|
3824
|
+
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
3825
|
+
return false;
|
|
3826
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
3827
|
+
return body?.stateKey === 'workspace-sync';
|
|
3828
|
+
});
|
|
3829
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
3830
|
+
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
3831
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
|
|
3832
|
+
});
|
|
3833
|
+
it('creates the first cloud workspace through the workspace creation flow during setup', async () => {
|
|
3834
|
+
const user = userEvent.setup();
|
|
3835
|
+
let workspaceCreated = false;
|
|
3836
|
+
const fetchMock = vi.fn((input, init) => {
|
|
3837
|
+
const url = String(input);
|
|
3838
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3839
|
+
return jsonResponse({
|
|
3840
|
+
success: true,
|
|
3841
|
+
config: {
|
|
3842
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3843
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3844
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3845
|
+
runtimeMode: 'cloud',
|
|
3846
|
+
authSource: 'cloud',
|
|
3847
|
+
workspaceMode: 'multi-cloud',
|
|
3848
|
+
workspaceSwitchingEnabled: true,
|
|
3849
|
+
cloudAuthViaLocalProxy: false
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3854
|
+
return jsonResponse({
|
|
3855
|
+
authenticated: true,
|
|
3856
|
+
runtimeMode: 'cloud',
|
|
3857
|
+
authRequiredForApi: true,
|
|
3858
|
+
userId: 'user-cloud-1',
|
|
3859
|
+
workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3863
|
+
return jsonResponse({
|
|
3864
|
+
gate: 'ok',
|
|
3865
|
+
hasEntitlement: true,
|
|
3866
|
+
canAccessApp: true,
|
|
3867
|
+
canAccessPlans: true,
|
|
3868
|
+
message: null
|
|
3869
|
+
});
|
|
3870
|
+
}
|
|
3871
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3872
|
+
return jsonResponse({
|
|
3873
|
+
...mockConfig,
|
|
3874
|
+
runtimeMode: 'cloud',
|
|
3875
|
+
authRequiredForApi: true,
|
|
3876
|
+
userId: 'user-cloud-1',
|
|
3877
|
+
projectName: workspaceCreated ? 'Cloud Mission' : '',
|
|
3878
|
+
setupState: {
|
|
3879
|
+
globalSetupState: 'present',
|
|
3880
|
+
workspaceSetupState: workspaceCreated ? 'present' : 'missing',
|
|
3881
|
+
runtimeMode: 'cloud',
|
|
3882
|
+
workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default',
|
|
3883
|
+
mode: 'core',
|
|
3884
|
+
forceSetup: false,
|
|
3885
|
+
forceGlobalSetup: false,
|
|
3886
|
+
forceWorkspaceSetup: false,
|
|
3887
|
+
workspace: workspaceCreated
|
|
3888
|
+
? { id: 'workspace-cloud-new', slug: 'workspace-cloud-new', name: 'Cloud Mission', description: 'Cloud workspace desc' }
|
|
3889
|
+
: null,
|
|
3890
|
+
suggestedWorkspaceName: ''
|
|
3891
|
+
},
|
|
3892
|
+
runtimeCapabilities: {
|
|
3893
|
+
runtimeMode: 'cloud',
|
|
3894
|
+
supportsCloudAuth: true,
|
|
3895
|
+
supportsAdminManagedSetupOverrides: true,
|
|
3896
|
+
supportsPackageScriptSetupOverrides: false
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
if (url.includes('/api/taskforce/workspaces') && String(init?.method || 'GET').toUpperCase() === 'GET') {
|
|
3901
|
+
return jsonResponse({
|
|
3902
|
+
success: true,
|
|
3903
|
+
workspaces: workspaceCreated
|
|
3904
|
+
? [{
|
|
3905
|
+
id: 'workspace-cloud-new',
|
|
3906
|
+
slug: 'workspace-cloud-new',
|
|
3907
|
+
name: 'Cloud Mission',
|
|
3908
|
+
description: 'Cloud workspace desc',
|
|
3909
|
+
role: 'owner',
|
|
3910
|
+
status: 'active',
|
|
3911
|
+
betaAccess: true
|
|
3912
|
+
}]
|
|
3913
|
+
: [],
|
|
3914
|
+
currentWorkspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
if (url.includes('/api/taskforce/workspaces') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
3918
|
+
workspaceCreated = true;
|
|
3919
|
+
return jsonResponse({
|
|
3920
|
+
success: true,
|
|
3921
|
+
workspace: {
|
|
3922
|
+
id: 'workspace-cloud-new',
|
|
3923
|
+
slug: 'workspace-cloud-new',
|
|
3924
|
+
name: 'Cloud Mission',
|
|
3925
|
+
description: 'Cloud workspace desc',
|
|
3926
|
+
role: 'owner',
|
|
3927
|
+
status: 'active',
|
|
3928
|
+
betaAccess: true
|
|
3929
|
+
}
|
|
3930
|
+
}, 201);
|
|
3931
|
+
}
|
|
3932
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3933
|
+
return jsonResponse({
|
|
3934
|
+
success: true,
|
|
3935
|
+
workspaceId: 'workspace-cloud-new',
|
|
3936
|
+
role: 'owner'
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3940
|
+
return jsonResponse({ success: true, state: {} });
|
|
3941
|
+
}
|
|
3942
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3943
|
+
return jsonResponse({
|
|
3944
|
+
success: true,
|
|
3945
|
+
state: {
|
|
3946
|
+
version: 2,
|
|
3947
|
+
enabled: false,
|
|
3948
|
+
phase: 'idle',
|
|
3949
|
+
pullCursor: null,
|
|
3950
|
+
lastPullAt: null,
|
|
3951
|
+
lastPushAt: null,
|
|
3952
|
+
lastSyncedAt: null
|
|
3953
|
+
}
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
3957
|
+
return jsonResponse({ success: true });
|
|
3958
|
+
}
|
|
3959
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
3960
|
+
return jsonResponse(mockCategories);
|
|
3961
|
+
}
|
|
3962
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3963
|
+
return jsonResponse({ tasks: mockTasks });
|
|
3964
|
+
}
|
|
3965
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
3966
|
+
return jsonResponse({ archived: [] });
|
|
3967
|
+
}
|
|
3968
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
3969
|
+
return jsonResponse({ dataVersion: 1 });
|
|
3970
|
+
}
|
|
3971
|
+
if (url.includes('/api/taskforce/types')) {
|
|
3972
|
+
return jsonResponse({ types: [] });
|
|
3973
|
+
}
|
|
3974
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
3975
|
+
return jsonResponse({ priorities: [] });
|
|
3976
|
+
}
|
|
3977
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
3978
|
+
return jsonResponse({ approaches: [] });
|
|
3979
|
+
}
|
|
3980
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
3981
|
+
return jsonResponse({ specialists: [] });
|
|
3982
|
+
}
|
|
3983
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3984
|
+
return jsonResponse({ templates: [] });
|
|
3985
|
+
}
|
|
3986
|
+
return jsonResponse({});
|
|
3987
|
+
});
|
|
3988
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3989
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
|
|
3990
|
+
await waitFor(() => {
|
|
3991
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3992
|
+
});
|
|
3993
|
+
expect(screen.getByLabelText('Cloud runtime')).toBeInTheDocument();
|
|
3994
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
3995
|
+
await user.clear(nameInput);
|
|
3996
|
+
await user.type(nameInput, 'Cloud Mission');
|
|
3997
|
+
await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Cloud workspace desc');
|
|
3998
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3999
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
4000
|
+
await waitFor(() => {
|
|
4001
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
4002
|
+
});
|
|
4003
|
+
expect(fetchMock.mock.calls.some((call) => (String(call[0]).includes('/api/taskforce/workspaces')
|
|
4004
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'))).toBe(true);
|
|
4005
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspace-profile'))).toBe(false);
|
|
4006
|
+
});
|
|
4007
|
+
it('applies the selected starter taxonomy pack during workspace setup', async () => {
|
|
4008
|
+
const user = userEvent.setup();
|
|
4009
|
+
let workspaceSaved = false;
|
|
4010
|
+
const fetchMock = vi.fn((url, init) => {
|
|
4011
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4012
|
+
return jsonResponse({
|
|
4013
|
+
authenticated: true,
|
|
4014
|
+
runtimeMode: 'local',
|
|
4015
|
+
authRequiredForApi: false
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4019
|
+
return jsonResponse({
|
|
4020
|
+
...mockConfig,
|
|
4021
|
+
runtimeMode: 'local',
|
|
4022
|
+
authRequiredForApi: false,
|
|
4023
|
+
setupState: {
|
|
4024
|
+
globalSetupState: 'present',
|
|
4025
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
4026
|
+
runtimeMode: 'local',
|
|
4027
|
+
workspaceId: workspaceSaved ? 'workspace-starter-1' : 'default',
|
|
4028
|
+
mode: 'core',
|
|
4029
|
+
forceSetup: false,
|
|
4030
|
+
forceGlobalSetup: false,
|
|
4031
|
+
forceWorkspaceSetup: true,
|
|
4032
|
+
workspace: workspaceSaved
|
|
4033
|
+
? { id: 'workspace-starter-1', slug: 'workspace-starter-1', name: 'Starter Workspace', description: 'Starter desc' }
|
|
4034
|
+
: null,
|
|
4035
|
+
suggestedWorkspaceName: 'Starter Workspace'
|
|
4036
|
+
},
|
|
4037
|
+
runtimeCapabilities: {
|
|
4038
|
+
runtimeMode: 'local',
|
|
4039
|
+
supportsCloudAuth: false,
|
|
4040
|
+
supportsAdminManagedSetupOverrides: false,
|
|
4041
|
+
supportsPackageScriptSetupOverrides: true
|
|
4042
|
+
}
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
4046
|
+
return jsonResponse({
|
|
4047
|
+
packs: [
|
|
4048
|
+
{
|
|
4049
|
+
id: 'general',
|
|
4050
|
+
label: 'General',
|
|
4051
|
+
description: 'Neutral starter pack.',
|
|
4052
|
+
isDefaultStarter: true,
|
|
4053
|
+
categories: [{ value: 'general', label: 'General', icon: 'Inbox', color: 'slate-400' }],
|
|
4054
|
+
types: [{ value: 'task', label: 'Task', icon: 'CheckSquare', color: 'blue-500' }],
|
|
4055
|
+
priorities: [{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' }]
|
|
4056
|
+
},
|
|
4057
|
+
{
|
|
4058
|
+
id: 'software-development',
|
|
4059
|
+
label: 'Software Development',
|
|
4060
|
+
description: 'Software starter pack.',
|
|
4061
|
+
categories: [{ value: 'engineering', label: 'Engineering', icon: 'Code', color: 'blue-500' }],
|
|
4062
|
+
types: [
|
|
4063
|
+
{ value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500' },
|
|
4064
|
+
{ value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500' }
|
|
4065
|
+
],
|
|
4066
|
+
priorities: [
|
|
4067
|
+
{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' },
|
|
4068
|
+
{ value: 2, label: 'Medium', icon: 'Minus', color: 'yellow-500' },
|
|
4069
|
+
{ value: 3, label: 'High', icon: 'ArrowUp', color: 'orange-500' },
|
|
4070
|
+
{ value: 4, label: 'Critical', icon: 'AlertTriangle', color: 'red-500' }
|
|
4071
|
+
]
|
|
4072
|
+
}
|
|
4073
|
+
]
|
|
4074
|
+
});
|
|
4075
|
+
}
|
|
4076
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
4077
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
4078
|
+
if (body?.name === 'Starter Workspace')
|
|
4079
|
+
workspaceSaved = true;
|
|
4080
|
+
return jsonResponse({
|
|
4081
|
+
success: true,
|
|
4082
|
+
workspace: {
|
|
4083
|
+
id: 'workspace-starter-1'
|
|
4084
|
+
}
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
if (url.includes('/api/taskforce/categories?workspaceId=workspace-starter-1')) {
|
|
4088
|
+
return jsonResponse({ success: true });
|
|
4089
|
+
}
|
|
4090
|
+
if (url.includes('/api/taskforce/types?workspaceId=workspace-starter-1')) {
|
|
4091
|
+
return jsonResponse({ success: true });
|
|
4092
|
+
}
|
|
4093
|
+
if (url.includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')) {
|
|
4094
|
+
return jsonResponse({ success: true });
|
|
4095
|
+
}
|
|
4096
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
4097
|
+
return jsonResponse({ success: true });
|
|
4098
|
+
}
|
|
4099
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4100
|
+
return jsonResponse({ tasks: mockTasks });
|
|
4101
|
+
}
|
|
4102
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
4103
|
+
return jsonResponse({ archived: [] });
|
|
4104
|
+
}
|
|
4105
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
4106
|
+
return jsonResponse({ templates: [] });
|
|
4107
|
+
}
|
|
4108
|
+
return jsonResponse({});
|
|
4109
|
+
});
|
|
4110
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4111
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
4112
|
+
await waitFor(() => {
|
|
4113
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4114
|
+
});
|
|
4115
|
+
expect(screen.getByLabelText('Local runtime')).toBeInTheDocument();
|
|
4116
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
4117
|
+
expect(screen.queryByRole('radio', { name: /create new local workspace/i })).not.toBeInTheDocument();
|
|
4118
|
+
expect(screen.queryByRole('radio', { name: /sync existing cloud workspace/i })).not.toBeInTheDocument();
|
|
4119
|
+
expect(screen.queryByText(/Includes \d+ categories?/i)).not.toBeInTheDocument();
|
|
4120
|
+
expect(screen.getByRole('option', { name: 'General' })).toBeInTheDocument();
|
|
4121
|
+
expect(screen.getByText('Task')).toBeInTheDocument();
|
|
4122
|
+
expect(screen.getByText('Low')).toBeInTheDocument();
|
|
4123
|
+
await user.selectOptions(screen.getByRole('combobox'), 'software-development');
|
|
4124
|
+
expect(screen.getByText('Engineering')).toBeInTheDocument();
|
|
4125
|
+
expect(screen.getByText('Feature, Bug')).toBeInTheDocument();
|
|
4126
|
+
expect(screen.getByText('Low, Medium, High, Critical')).toBeInTheDocument();
|
|
4127
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
4128
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
4129
|
+
await waitFor(() => {
|
|
4130
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
4131
|
+
});
|
|
4132
|
+
const categoryApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/categories?workspaceId=workspace-starter-1')
|
|
4133
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
4134
|
+
const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-starter-1')
|
|
4135
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
4136
|
+
const priorityApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')
|
|
4137
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
4138
|
+
expect(categoryApplyCall).toBeDefined();
|
|
4139
|
+
expect(typeApplyCall).toBeDefined();
|
|
4140
|
+
expect(priorityApplyCall).toBeDefined();
|
|
4141
|
+
expect(new Headers(categoryApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
4142
|
+
expect(new Headers(typeApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
4143
|
+
expect(new Headers(priorityApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
4144
|
+
});
|
|
4145
|
+
it('recovers from transient local workspace-missing setup state after scope resolves on refresh', async () => {
|
|
4146
|
+
let configCalls = 0;
|
|
4147
|
+
const fetchMock = vi.fn((url) => {
|
|
4148
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4149
|
+
return Promise.resolve({
|
|
4150
|
+
ok: true,
|
|
4151
|
+
json: () => Promise.resolve({
|
|
4152
|
+
authenticated: false,
|
|
4153
|
+
runtimeMode: 'local',
|
|
4154
|
+
authRequiredForApi: false
|
|
4155
|
+
})
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4159
|
+
configCalls += 1;
|
|
4160
|
+
const firstPass = configCalls === 1;
|
|
4161
|
+
return Promise.resolve({
|
|
4162
|
+
ok: true,
|
|
4163
|
+
json: () => Promise.resolve({
|
|
4164
|
+
...mockConfig,
|
|
4165
|
+
runtimeMode: 'local',
|
|
4166
|
+
authRequiredForApi: false,
|
|
4167
|
+
projectRoot: '/mock/local-project-refresh',
|
|
4168
|
+
workspaceId: 'workspace-local-refresh',
|
|
4169
|
+
setupState: {
|
|
4170
|
+
globalSetupState: 'present',
|
|
4171
|
+
workspaceSetupState: firstPass ? 'missing' : 'present',
|
|
4172
|
+
runtimeMode: 'local',
|
|
4173
|
+
workspaceId: 'workspace-local-refresh',
|
|
4174
|
+
mode: 'core',
|
|
4175
|
+
forceSetup: false,
|
|
4176
|
+
forceGlobalSetup: false,
|
|
4177
|
+
forceWorkspaceSetup: firstPass,
|
|
4178
|
+
workspace: firstPass
|
|
4179
|
+
? null
|
|
4180
|
+
: { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
|
|
4181
|
+
suggestedWorkspaceName: 'Local Refresh Project'
|
|
4182
|
+
},
|
|
4183
|
+
runtimeCapabilities: {
|
|
4184
|
+
runtimeMode: 'local',
|
|
4185
|
+
supportsCloudAuth: true,
|
|
4186
|
+
supportsAdminManagedSetupOverrides: false,
|
|
4187
|
+
supportsPackageScriptSetupOverrides: true
|
|
4188
|
+
}
|
|
4189
|
+
})
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
4193
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
4194
|
+
}
|
|
4195
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4196
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
4197
|
+
}
|
|
4198
|
+
if (url.includes('/api/taskforce/types')) {
|
|
4199
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
4200
|
+
}
|
|
4201
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
4202
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
4203
|
+
}
|
|
4204
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
4205
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
4206
|
+
}
|
|
4207
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
4208
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
4209
|
+
}
|
|
4210
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
4211
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4212
|
+
}
|
|
4213
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4214
|
+
});
|
|
4215
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4216
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
4217
|
+
await waitFor(() => {
|
|
4218
|
+
expect(configCalls).toBeGreaterThan(1);
|
|
4219
|
+
});
|
|
4220
|
+
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
4221
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
4222
|
+
});
|
|
4223
|
+
it('keeps setup blocking while the selected cloud workspace is performing its first local import', async () => {
|
|
4224
|
+
const user = userEvent.setup();
|
|
4225
|
+
let workspaceSaved = false;
|
|
4226
|
+
const fetchMock = vi.fn((url, init) => {
|
|
4227
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4228
|
+
return Promise.resolve({
|
|
4229
|
+
ok: true,
|
|
4230
|
+
json: () => Promise.resolve({
|
|
4231
|
+
authenticated: true,
|
|
4232
|
+
runtimeMode: 'local',
|
|
4233
|
+
authRequiredForApi: false
|
|
4234
|
+
})
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4238
|
+
return Promise.resolve({
|
|
4239
|
+
ok: true,
|
|
4240
|
+
json: () => Promise.resolve({
|
|
4241
|
+
...mockConfig,
|
|
4242
|
+
runtimeMode: 'local',
|
|
4243
|
+
authRequiredForApi: false,
|
|
4244
|
+
setupState: {
|
|
4245
|
+
globalSetupState: 'present',
|
|
4246
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
4247
|
+
runtimeMode: 'local',
|
|
4248
|
+
workspaceId: workspaceSaved ? 'workspace-cloud-123' : 'default',
|
|
4249
|
+
mode: 'core',
|
|
4250
|
+
forceSetup: false,
|
|
4251
|
+
forceGlobalSetup: false,
|
|
4252
|
+
forceWorkspaceSetup: true,
|
|
4253
|
+
workspace: workspaceSaved
|
|
4254
|
+
? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
|
|
4255
|
+
: null,
|
|
4256
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
4257
|
+
},
|
|
4258
|
+
runtimeCapabilities: {
|
|
4259
|
+
runtimeMode: 'local',
|
|
4260
|
+
supportsCloudAuth: true,
|
|
4261
|
+
supportsAdminManagedSetupOverrides: false,
|
|
4262
|
+
supportsPackageScriptSetupOverrides: true
|
|
4263
|
+
}
|
|
4264
|
+
})
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
4268
|
+
return Promise.resolve({
|
|
4269
|
+
ok: true,
|
|
4270
|
+
json: () => Promise.resolve({
|
|
4271
|
+
success: true,
|
|
4272
|
+
workspaces: [
|
|
4273
|
+
{
|
|
4274
|
+
id: 'workspace-cloud-123',
|
|
4275
|
+
name: 'Cloud Project 123',
|
|
4276
|
+
description: 'Cloud desc'
|
|
4277
|
+
}
|
|
4278
|
+
]
|
|
4279
|
+
})
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
4283
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
4284
|
+
if (body?.workspaceId === 'workspace-cloud-123')
|
|
4285
|
+
workspaceSaved = true;
|
|
4286
|
+
return Promise.resolve({
|
|
4287
|
+
ok: true,
|
|
4288
|
+
json: () => Promise.resolve({
|
|
4289
|
+
success: true,
|
|
4290
|
+
workspace: {
|
|
4291
|
+
id: 'workspace-cloud-123'
|
|
4292
|
+
}
|
|
4293
|
+
})
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
4297
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4298
|
+
}
|
|
4299
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4300
|
+
return Promise.resolve({
|
|
4301
|
+
ok: true,
|
|
4302
|
+
json: () => Promise.resolve({
|
|
4303
|
+
success: true,
|
|
4304
|
+
state: workspaceSaved
|
|
4305
|
+
? {
|
|
4306
|
+
version: 2,
|
|
4307
|
+
enabled: true,
|
|
4308
|
+
phase: 'attach-cloud',
|
|
4309
|
+
pullCursor: null,
|
|
4310
|
+
lastPullAt: null,
|
|
4311
|
+
lastPushAt: null,
|
|
4312
|
+
lastSyncedAt: null
|
|
4313
|
+
}
|
|
4314
|
+
: {
|
|
4315
|
+
version: 2,
|
|
4316
|
+
enabled: false,
|
|
4317
|
+
phase: 'idle',
|
|
4318
|
+
pullCursor: null,
|
|
4319
|
+
lastPullAt: null,
|
|
4320
|
+
lastPushAt: null,
|
|
4321
|
+
lastSyncedAt: null
|
|
4322
|
+
}
|
|
4323
|
+
})
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
4327
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4328
|
+
}
|
|
4329
|
+
if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
|
|
4330
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4331
|
+
}
|
|
4332
|
+
if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
|
|
4333
|
+
return new Promise(() => { });
|
|
4334
|
+
}
|
|
4335
|
+
if (url === '/api/taskforce/sync/workspace/apply-local') {
|
|
4336
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4337
|
+
}
|
|
4338
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4339
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
4340
|
+
}
|
|
4341
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
4342
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4343
|
+
}
|
|
4344
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4345
|
+
});
|
|
4346
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4347
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4348
|
+
await waitFor(() => {
|
|
4349
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4350
|
+
});
|
|
4351
|
+
await waitFor(() => {
|
|
4352
|
+
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
4353
|
+
});
|
|
4354
|
+
await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
|
|
4355
|
+
await user.click(screen.getByRole('button', { name: 'Connect Workspace' }));
|
|
4356
|
+
await waitFor(() => {
|
|
4357
|
+
expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
|
|
4358
|
+
});
|
|
4359
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4360
|
+
expect(screen.getByRole('button', { name: 'Importing Workspace...' })).toBeInTheDocument();
|
|
4361
|
+
const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
|
|
4362
|
+
const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
|
|
4363
|
+
expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
|
|
4364
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
4365
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
4366
|
+
return false;
|
|
4367
|
+
const init = call[1];
|
|
4368
|
+
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
4369
|
+
return false;
|
|
4370
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
4371
|
+
return body?.stateKey === 'workspace-sync';
|
|
4372
|
+
});
|
|
4373
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
4374
|
+
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
4375
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
|
|
4376
|
+
expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
|
|
4377
|
+
expect(workspaceSyncStateBody.patch?.setupIntent).toBe('attach-cloud-import');
|
|
4378
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
|
|
4379
|
+
expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
|
|
4380
|
+
});
|
|
4381
|
+
it('keeps attach-cloud import blocking after refresh when setup intent is persisted', async () => {
|
|
4382
|
+
const fetchMock = vi.fn((url) => {
|
|
4383
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4384
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
4385
|
+
}
|
|
4386
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4387
|
+
return Promise.resolve({
|
|
4388
|
+
ok: true,
|
|
4389
|
+
json: () => Promise.resolve({
|
|
4390
|
+
authenticated: true,
|
|
4391
|
+
runtimeMode: 'local',
|
|
4392
|
+
authRequiredForApi: false
|
|
4393
|
+
})
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4397
|
+
return Promise.resolve({
|
|
4398
|
+
ok: true,
|
|
4399
|
+
json: () => Promise.resolve({
|
|
4400
|
+
...mockConfig,
|
|
4401
|
+
runtimeMode: 'local',
|
|
4402
|
+
authRequiredForApi: false,
|
|
4403
|
+
workspaceId: 'workspace-cloud-123',
|
|
4404
|
+
setupState: {
|
|
4405
|
+
globalSetupState: 'present',
|
|
4406
|
+
workspaceSetupState: 'missing',
|
|
4407
|
+
runtimeMode: 'local',
|
|
4408
|
+
workspaceId: 'workspace-cloud-123',
|
|
4409
|
+
mode: 'core',
|
|
4410
|
+
forceSetup: false,
|
|
4411
|
+
forceGlobalSetup: false,
|
|
4412
|
+
forceWorkspaceSetup: true,
|
|
4413
|
+
workspace: null,
|
|
4414
|
+
suggestedWorkspaceName: 'Cloud Project 123'
|
|
4415
|
+
},
|
|
4416
|
+
runtimeCapabilities: {
|
|
4417
|
+
runtimeMode: 'local',
|
|
4418
|
+
supportsCloudAuth: true,
|
|
4419
|
+
supportsAdminManagedSetupOverrides: false,
|
|
4420
|
+
supportsPackageScriptSetupOverrides: true
|
|
4421
|
+
}
|
|
4422
|
+
})
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4426
|
+
return Promise.resolve({
|
|
4427
|
+
ok: true,
|
|
4428
|
+
json: () => Promise.resolve({
|
|
4429
|
+
success: true,
|
|
4430
|
+
state: {
|
|
4431
|
+
version: 2,
|
|
4432
|
+
enabled: true,
|
|
4433
|
+
phase: 'attach-cloud',
|
|
4434
|
+
setupIntent: 'attach-cloud-import',
|
|
4435
|
+
pullCursor: null,
|
|
4436
|
+
lastPullAt: null,
|
|
4437
|
+
lastPushAt: null,
|
|
4438
|
+
lastSyncedAt: null
|
|
4439
|
+
}
|
|
4440
|
+
})
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
4444
|
+
return Promise.resolve({
|
|
4445
|
+
ok: true,
|
|
4446
|
+
json: () => Promise.resolve({
|
|
4447
|
+
success: true,
|
|
4448
|
+
workspaces: [
|
|
4449
|
+
{
|
|
4450
|
+
id: 'workspace-cloud-123',
|
|
4451
|
+
name: 'Cloud Project 123',
|
|
4452
|
+
description: 'Cloud desc'
|
|
4453
|
+
}
|
|
4454
|
+
]
|
|
4455
|
+
})
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4459
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
4460
|
+
}
|
|
4461
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4462
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
4463
|
+
}
|
|
4464
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
4465
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
4466
|
+
}
|
|
4467
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
4468
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
4469
|
+
}
|
|
4470
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
4471
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4472
|
+
}
|
|
4473
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
4474
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
4475
|
+
}
|
|
4476
|
+
if (url.includes('/api/taskforce/types'))
|
|
4477
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
4478
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
4479
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
4480
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
4481
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
4482
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
4483
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
4484
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
4485
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4486
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4487
|
+
});
|
|
4488
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4489
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4490
|
+
await waitFor(() => {
|
|
4491
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4492
|
+
});
|
|
4493
|
+
await waitFor(() => {
|
|
4494
|
+
expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
|
|
4495
|
+
});
|
|
4496
|
+
});
|
|
4497
|
+
it('clears persisted attach-cloud import intent after the first successful import', async () => {
|
|
4498
|
+
const uiStatePosts = [];
|
|
4499
|
+
const fetchMock = vi.fn((url, init) => {
|
|
4500
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4501
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
4502
|
+
}
|
|
4503
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4504
|
+
return Promise.resolve({
|
|
4505
|
+
ok: true,
|
|
4506
|
+
json: () => Promise.resolve({
|
|
4507
|
+
authenticated: true,
|
|
4508
|
+
runtimeMode: 'local',
|
|
4509
|
+
authRequiredForApi: false
|
|
4510
|
+
})
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4514
|
+
return Promise.resolve({
|
|
4515
|
+
ok: true,
|
|
4516
|
+
json: () => Promise.resolve({
|
|
4517
|
+
...mockConfig,
|
|
4518
|
+
runtimeMode: 'local',
|
|
4519
|
+
authRequiredForApi: false,
|
|
4520
|
+
workspaceId: 'workspace-cloud-123'
|
|
4521
|
+
})
|
|
4522
|
+
});
|
|
4523
|
+
}
|
|
4524
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4525
|
+
return Promise.resolve({
|
|
4526
|
+
ok: true,
|
|
4527
|
+
json: () => Promise.resolve({
|
|
4528
|
+
success: true,
|
|
4529
|
+
state: {
|
|
4530
|
+
version: 2,
|
|
4531
|
+
enabled: true,
|
|
4532
|
+
phase: 'active',
|
|
4533
|
+
setupIntent: 'attach-cloud-import',
|
|
4534
|
+
pullCursor: 'cursor-1',
|
|
4535
|
+
lastPullAt: '2026-04-15T12:00:00.000Z',
|
|
4536
|
+
lastPushAt: null,
|
|
4537
|
+
lastSyncedAt: '2026-04-15T12:00:00.000Z'
|
|
4538
|
+
}
|
|
4539
|
+
})
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
4543
|
+
uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
|
|
4544
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4545
|
+
}
|
|
4546
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
4547
|
+
return Promise.resolve({
|
|
4548
|
+
ok: true,
|
|
4549
|
+
json: () => Promise.resolve({
|
|
4550
|
+
success: true,
|
|
4551
|
+
workspaces: [
|
|
4552
|
+
{
|
|
4553
|
+
id: 'workspace-cloud-123',
|
|
4554
|
+
name: 'Cloud Project 123',
|
|
4555
|
+
description: 'Cloud desc'
|
|
4556
|
+
}
|
|
4557
|
+
]
|
|
4558
|
+
})
|
|
2872
4559
|
});
|
|
2873
4560
|
}
|
|
2874
|
-
if (url.includes('/api/taskforce/
|
|
2875
|
-
return
|
|
2876
|
-
}
|
|
2877
|
-
if (url.includes('/api/taskforce/types?workspaceId=workspace-starter-1')) {
|
|
2878
|
-
return jsonResponse({ success: true });
|
|
2879
|
-
}
|
|
2880
|
-
if (url.includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')) {
|
|
2881
|
-
return jsonResponse({ success: true });
|
|
2882
|
-
}
|
|
2883
|
-
if (url.includes('/api/taskforce/ui-state')) {
|
|
2884
|
-
return jsonResponse({ success: true });
|
|
4561
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4562
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
2885
4563
|
}
|
|
2886
4564
|
if (url.includes('/api/taskforce/tasks')) {
|
|
2887
|
-
return
|
|
4565
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2888
4566
|
}
|
|
2889
4567
|
if (url.includes('/api/taskforce/archive')) {
|
|
2890
|
-
return
|
|
4568
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
2891
4569
|
}
|
|
2892
|
-
if (url.includes('/api/taskforce/
|
|
2893
|
-
return
|
|
4570
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
4571
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
2894
4572
|
}
|
|
2895
|
-
|
|
4573
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
4574
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4575
|
+
}
|
|
4576
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
4577
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
4578
|
+
}
|
|
4579
|
+
if (url.includes('/api/taskforce/types'))
|
|
4580
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
4581
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
4582
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
4583
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
4584
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
4585
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
4586
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
4587
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
4588
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4589
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2896
4590
|
});
|
|
2897
4591
|
vi.stubGlobal('fetch', fetchMock);
|
|
2898
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
4592
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2899
4593
|
await waitFor(() => {
|
|
2900
|
-
expect(screen.getByText('
|
|
4594
|
+
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
|
2901
4595
|
});
|
|
2902
|
-
await user.click(screen.getByRole('radio', { name: /software development/i }));
|
|
2903
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2904
4596
|
await waitFor(() => {
|
|
2905
|
-
expect(
|
|
4597
|
+
expect(uiStatePosts.some((entry) => entry?.stateKey === 'workspace-sync' && entry?.patch?.setupIntent === null)).toBe(true);
|
|
2906
4598
|
});
|
|
2907
|
-
const categoryApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/categories?workspaceId=workspace-starter-1')
|
|
2908
|
-
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2909
|
-
const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-starter-1')
|
|
2910
|
-
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2911
|
-
const priorityApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')
|
|
2912
|
-
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2913
|
-
expect(categoryApplyCall).toBeDefined();
|
|
2914
|
-
expect(typeApplyCall).toBeDefined();
|
|
2915
|
-
expect(priorityApplyCall).toBeDefined();
|
|
2916
|
-
expect(new Headers(categoryApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2917
|
-
expect(new Headers(typeApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2918
|
-
expect(new Headers(priorityApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2919
4599
|
});
|
|
2920
|
-
it('
|
|
2921
|
-
let configCalls = 0;
|
|
4600
|
+
it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
|
|
2922
4601
|
const fetchMock = vi.fn((url) => {
|
|
2923
4602
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
2924
4603
|
return Promise.resolve({
|
|
@@ -2931,29 +4610,23 @@ describe('TaskforceCore Integration', () => {
|
|
|
2931
4610
|
});
|
|
2932
4611
|
}
|
|
2933
4612
|
if (url.includes('/api/taskforce/config')) {
|
|
2934
|
-
configCalls += 1;
|
|
2935
|
-
const firstPass = configCalls === 1;
|
|
2936
4613
|
return Promise.resolve({
|
|
2937
4614
|
ok: true,
|
|
2938
4615
|
json: () => Promise.resolve({
|
|
2939
4616
|
...mockConfig,
|
|
2940
4617
|
runtimeMode: 'local',
|
|
2941
4618
|
authRequiredForApi: false,
|
|
2942
|
-
projectRoot: '/mock/local-project-refresh',
|
|
2943
|
-
workspaceId: 'workspace-local-refresh',
|
|
2944
4619
|
setupState: {
|
|
2945
4620
|
globalSetupState: 'present',
|
|
2946
|
-
workspaceSetupState:
|
|
4621
|
+
workspaceSetupState: 'missing',
|
|
2947
4622
|
runtimeMode: 'local',
|
|
2948
|
-
workspaceId: '
|
|
4623
|
+
workspaceId: 'default',
|
|
2949
4624
|
mode: 'core',
|
|
2950
4625
|
forceSetup: false,
|
|
2951
4626
|
forceGlobalSetup: false,
|
|
2952
|
-
forceWorkspaceSetup:
|
|
2953
|
-
workspace:
|
|
2954
|
-
|
|
2955
|
-
: { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
|
|
2956
|
-
suggestedWorkspaceName: 'Local Refresh Project'
|
|
4627
|
+
forceWorkspaceSetup: true,
|
|
4628
|
+
workspace: null,
|
|
4629
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
2957
4630
|
},
|
|
2958
4631
|
runtimeCapabilities: {
|
|
2959
4632
|
runtimeMode: 'local',
|
|
@@ -2964,46 +4637,26 @@ describe('TaskforceCore Integration', () => {
|
|
|
2964
4637
|
})
|
|
2965
4638
|
});
|
|
2966
4639
|
}
|
|
2967
|
-
if (url.includes('/api/taskforce/categories')) {
|
|
2968
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
2969
|
-
}
|
|
2970
|
-
if (url.includes('/api/taskforce/tasks')) {
|
|
2971
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2972
|
-
}
|
|
2973
|
-
if (url.includes('/api/taskforce/types')) {
|
|
2974
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
2975
|
-
}
|
|
2976
|
-
if (url.includes('/api/taskforce/priorities')) {
|
|
2977
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
2978
|
-
}
|
|
2979
|
-
if (url.includes('/api/taskforce/approaches')) {
|
|
2980
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
2981
|
-
}
|
|
2982
|
-
if (url.includes('/api/taskforce/specialists')) {
|
|
2983
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
2984
|
-
}
|
|
2985
|
-
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2986
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2987
|
-
}
|
|
2988
4640
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2989
4641
|
});
|
|
2990
4642
|
vi.stubGlobal('fetch', fetchMock);
|
|
2991
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
4643
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2992
4644
|
await waitFor(() => {
|
|
2993
|
-
expect(
|
|
4645
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2994
4646
|
});
|
|
2995
|
-
expect(screen.getByRole('button', { name:
|
|
2996
|
-
expect(screen.
|
|
4647
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
4648
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
4649
|
+
expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeDisabled();
|
|
2997
4650
|
});
|
|
2998
|
-
it('
|
|
4651
|
+
it('returns signed-in users to the cloud workspace picker after setup sign-in', async () => {
|
|
2999
4652
|
const user = userEvent.setup();
|
|
3000
|
-
let
|
|
4653
|
+
let authenticated = false;
|
|
3001
4654
|
const fetchMock = vi.fn((url, init) => {
|
|
3002
4655
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
3003
4656
|
return Promise.resolve({
|
|
3004
4657
|
ok: true,
|
|
3005
4658
|
json: () => Promise.resolve({
|
|
3006
|
-
authenticated
|
|
4659
|
+
authenticated,
|
|
3007
4660
|
runtimeMode: 'local',
|
|
3008
4661
|
authRequiredForApi: false
|
|
3009
4662
|
})
|
|
@@ -3018,16 +4671,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
3018
4671
|
authRequiredForApi: false,
|
|
3019
4672
|
setupState: {
|
|
3020
4673
|
globalSetupState: 'present',
|
|
3021
|
-
workspaceSetupState:
|
|
4674
|
+
workspaceSetupState: 'missing',
|
|
3022
4675
|
runtimeMode: 'local',
|
|
3023
|
-
workspaceId:
|
|
4676
|
+
workspaceId: 'default',
|
|
3024
4677
|
mode: 'core',
|
|
3025
4678
|
forceSetup: false,
|
|
3026
4679
|
forceGlobalSetup: false,
|
|
3027
4680
|
forceWorkspaceSetup: true,
|
|
3028
|
-
workspace:
|
|
3029
|
-
? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
|
|
3030
|
-
: null,
|
|
4681
|
+
workspace: null,
|
|
3031
4682
|
suggestedWorkspaceName: 'Suggested Project'
|
|
3032
4683
|
},
|
|
3033
4684
|
runtimeCapabilities: {
|
|
@@ -3039,6 +4690,16 @@ describe('TaskforceCore Integration', () => {
|
|
|
3039
4690
|
})
|
|
3040
4691
|
});
|
|
3041
4692
|
}
|
|
4693
|
+
if (url.includes('/api/taskforce/auth/login') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
4694
|
+
authenticated = true;
|
|
4695
|
+
return Promise.resolve({
|
|
4696
|
+
ok: true,
|
|
4697
|
+
json: () => Promise.resolve({
|
|
4698
|
+
success: true,
|
|
4699
|
+
workspaceSetupRequired: true
|
|
4700
|
+
})
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
3042
4703
|
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
3043
4704
|
return Promise.resolve({
|
|
3044
4705
|
ok: true,
|
|
@@ -3054,64 +4715,135 @@ describe('TaskforceCore Integration', () => {
|
|
|
3054
4715
|
})
|
|
3055
4716
|
});
|
|
3056
4717
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
4718
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4719
|
+
});
|
|
4720
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4721
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4722
|
+
await waitFor(() => {
|
|
4723
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
4724
|
+
});
|
|
4725
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
4726
|
+
await waitFor(() => {
|
|
4727
|
+
expect(screen.getByTestId('auth-email-input')).toBeInTheDocument();
|
|
4728
|
+
});
|
|
4729
|
+
await user.type(screen.getByTestId('auth-email-input'), 'agent@example.com');
|
|
4730
|
+
await user.type(screen.getByTestId('auth-password-input'), 'password123');
|
|
4731
|
+
await user.click(screen.getByTestId('auth-submit-button'));
|
|
4732
|
+
await waitFor(() => {
|
|
4733
|
+
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
4734
|
+
});
|
|
4735
|
+
await waitFor(() => {
|
|
4736
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
4737
|
+
expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeEnabled();
|
|
4738
|
+
});
|
|
4739
|
+
});
|
|
4740
|
+
it('loads workspace setup after cloud sign-in when the user has no workspaces', async () => {
|
|
4741
|
+
const user = userEvent.setup();
|
|
4742
|
+
let authenticated = false;
|
|
4743
|
+
const fetchMock = vi.fn((input, init) => {
|
|
4744
|
+
const url = String(input);
|
|
4745
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4746
|
+
return jsonResponse({
|
|
4747
|
+
success: true,
|
|
4748
|
+
config: {
|
|
4749
|
+
baseUrl: 'https://app.taskforce.example',
|
|
4750
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
4751
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
4752
|
+
runtimeMode: 'cloud',
|
|
4753
|
+
authSource: 'cloud',
|
|
4754
|
+
workspaceMode: 'multi-cloud',
|
|
4755
|
+
workspaceSwitchingEnabled: true,
|
|
4756
|
+
cloudAuthViaLocalProxy: false
|
|
4757
|
+
}
|
|
3069
4758
|
});
|
|
3070
4759
|
}
|
|
3071
|
-
if (url.includes('/api/taskforce/session
|
|
3072
|
-
return
|
|
4760
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4761
|
+
return jsonResponse({
|
|
4762
|
+
authenticated,
|
|
4763
|
+
runtimeMode: 'cloud',
|
|
4764
|
+
authRequiredForApi: true,
|
|
4765
|
+
userId: authenticated ? 'user-cloud-setup' : 'anonymous',
|
|
4766
|
+
workspaceId: 'default'
|
|
4767
|
+
});
|
|
3073
4768
|
}
|
|
3074
|
-
if (url.includes('/api/taskforce/
|
|
3075
|
-
|
|
4769
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
4770
|
+
authenticated = true;
|
|
4771
|
+
return jsonResponse({ success: true });
|
|
3076
4772
|
}
|
|
3077
|
-
if (url.includes('/api/taskforce/
|
|
3078
|
-
return
|
|
4773
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
4774
|
+
return jsonResponse({
|
|
4775
|
+
success: true,
|
|
4776
|
+
workspaces: [],
|
|
4777
|
+
currentWorkspaceId: 'default'
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
4781
|
+
return jsonResponse({
|
|
4782
|
+
gate: 'ok',
|
|
4783
|
+
hasEntitlement: true,
|
|
4784
|
+
canAccessApp: true,
|
|
4785
|
+
canAccessPlans: true,
|
|
4786
|
+
message: null
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4790
|
+
return jsonResponse({
|
|
4791
|
+
...mockConfig,
|
|
4792
|
+
runtimeMode: 'cloud',
|
|
4793
|
+
authRequiredForApi: true,
|
|
4794
|
+
userId: authenticated ? 'user-cloud-setup' : 'anonymous',
|
|
4795
|
+
setupState: {
|
|
4796
|
+
globalSetupState: 'present',
|
|
4797
|
+
workspaceSetupState: authenticated ? 'missing' : 'present',
|
|
4798
|
+
runtimeMode: 'cloud',
|
|
4799
|
+
workspaceId: 'default',
|
|
4800
|
+
mode: 'core',
|
|
4801
|
+
forceSetup: false,
|
|
4802
|
+
forceGlobalSetup: false,
|
|
4803
|
+
forceWorkspaceSetup: false,
|
|
4804
|
+
workspace: null,
|
|
4805
|
+
suggestedWorkspaceName: ''
|
|
4806
|
+
},
|
|
4807
|
+
runtimeCapabilities: {
|
|
4808
|
+
runtimeMode: 'cloud',
|
|
4809
|
+
supportsCloudAuth: true,
|
|
4810
|
+
supportsAdminManagedSetupOverrides: true,
|
|
4811
|
+
supportsPackageScriptSetupOverrides: false
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4816
|
+
return jsonResponse({ success: true, state: {} });
|
|
4817
|
+
}
|
|
4818
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4819
|
+
return jsonResponse({
|
|
4820
|
+
success: true,
|
|
4821
|
+
state: {
|
|
4822
|
+
version: 2,
|
|
4823
|
+
enabled: false,
|
|
4824
|
+
phase: 'idle',
|
|
4825
|
+
pullCursor: null,
|
|
4826
|
+
lastPullAt: null,
|
|
4827
|
+
lastPushAt: null,
|
|
4828
|
+
lastSyncedAt: null
|
|
4829
|
+
}
|
|
4830
|
+
});
|
|
3079
4831
|
}
|
|
3080
4832
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3081
|
-
return
|
|
4833
|
+
return jsonResponse({ templates: [] });
|
|
3082
4834
|
}
|
|
3083
|
-
return
|
|
4835
|
+
return jsonResponse({});
|
|
3084
4836
|
});
|
|
3085
4837
|
vi.stubGlobal('fetch', fetchMock);
|
|
3086
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/
|
|
3087
|
-
await
|
|
3088
|
-
|
|
3089
|
-
});
|
|
3090
|
-
await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
|
|
3091
|
-
await waitFor(() => {
|
|
3092
|
-
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
3093
|
-
});
|
|
3094
|
-
await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
|
|
3095
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
4838
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=login&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
4839
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'setup@example.com');
|
|
4840
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
4841
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
3096
4842
|
await waitFor(() => {
|
|
3097
|
-
expect(screen.
|
|
3098
|
-
});
|
|
3099
|
-
const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
|
|
3100
|
-
const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
|
|
3101
|
-
expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
|
|
3102
|
-
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
3103
|
-
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
3104
|
-
return false;
|
|
3105
|
-
const init = call[1];
|
|
3106
|
-
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
3107
|
-
return false;
|
|
3108
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
3109
|
-
return body?.stateKey === 'workspace-sync';
|
|
4843
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3110
4844
|
});
|
|
3111
|
-
|
|
3112
|
-
expect(
|
|
3113
|
-
expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
|
|
3114
|
-
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
|
|
4845
|
+
expect(screen.queryByText('Resolving workspace access...')).not.toBeInTheDocument();
|
|
4846
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/config'))).toBe(true);
|
|
3115
4847
|
});
|
|
3116
4848
|
it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
|
|
3117
4849
|
const user = userEvent.setup();
|
|
@@ -3183,16 +4915,17 @@ describe('TaskforceCore Integration', () => {
|
|
|
3183
4915
|
vi.stubGlobal('fetch', fetchMock);
|
|
3184
4916
|
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3185
4917
|
await waitFor(() => {
|
|
3186
|
-
expect(screen.getByText('
|
|
4918
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3187
4919
|
});
|
|
3188
4920
|
await waitFor(() => {
|
|
3189
4921
|
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
3190
4922
|
});
|
|
3191
4923
|
await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
|
|
3192
|
-
const nameInput = screen.getByPlaceholderText('
|
|
4924
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
3193
4925
|
await user.clear(nameInput);
|
|
3194
4926
|
await user.type(nameInput, 'Local Project');
|
|
3195
|
-
await user.click(screen.getByRole('button', { name: '
|
|
4927
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
4928
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
3196
4929
|
await waitFor(() => {
|
|
3197
4930
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3198
4931
|
});
|
|
@@ -3206,10 +4939,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
3206
4939
|
return body?.stateKey === 'workspace-sync';
|
|
3207
4940
|
});
|
|
3208
4941
|
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
3209
|
-
expect(workspaceSyncStateBody.patch?.
|
|
3210
|
-
expect(workspaceSyncStateBody.patch?.
|
|
3211
|
-
|
|
3212
|
-
expect(workspaceSyncStateBody.patch?.
|
|
4942
|
+
expect(workspaceSyncStateBody.patch?.version).toBe(2);
|
|
4943
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
|
|
4944
|
+
expect(workspaceSyncStateBody.patch?.phase).toBe('provision-local');
|
|
4945
|
+
expect(workspaceSyncStateBody.patch?.setupIntent).toBeNull();
|
|
4946
|
+
expect(workspaceSyncStateBody.patch?.pullCursor).toBeNull();
|
|
3213
4947
|
});
|
|
3214
4948
|
it('creates a new cloud workspace only after guided setup submit', async () => {
|
|
3215
4949
|
const user = userEvent.setup();
|
|
@@ -3621,7 +5355,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
3621
5355
|
vi.stubGlobal('fetch', fetchMock);
|
|
3622
5356
|
const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3623
5357
|
await waitFor(() => {
|
|
3624
|
-
expect(screen.getByText('
|
|
5358
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3625
5359
|
});
|
|
3626
5360
|
await waitFor(() => {
|
|
3627
5361
|
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
@@ -3637,7 +5371,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
3637
5371
|
initialRender.unmount();
|
|
3638
5372
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3639
5373
|
await waitFor(() => {
|
|
3640
|
-
expect(screen.queryByText('
|
|
5374
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
3641
5375
|
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
3642
5376
|
});
|
|
3643
5377
|
expect(cloudWorkspaceCreated).toBe(false);
|
|
@@ -3878,18 +5612,31 @@ describe('TaskforceCore Integration', () => {
|
|
|
3878
5612
|
vi.stubGlobal('fetch', fetchMock);
|
|
3879
5613
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3880
5614
|
await waitFor(() => {
|
|
3881
|
-
expect(screen.getByTitle(
|
|
5615
|
+
expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
|
|
3882
5616
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3883
5617
|
});
|
|
3884
5618
|
await user.click(screen.getByTitle(/Account/i));
|
|
3885
5619
|
await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
|
|
3886
5620
|
await waitFor(() => {
|
|
3887
5621
|
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
|
|
3888
|
-
expect(screen.getByTitle(
|
|
5622
|
+
expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
|
|
3889
5623
|
});
|
|
3890
5624
|
await user.click(screen.getByTitle(/Account/i));
|
|
3891
5625
|
expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
|
|
3892
5626
|
});
|
|
5627
|
+
it('uses an explicit cloud sign-out redirect and broader cloud logout reset path', () => {
|
|
5628
|
+
const standaloneLayoutSource = fs.readFileSync(path.join(__dirname, 'components/views/StandaloneLayout.tsx'), 'utf8');
|
|
5629
|
+
const useTaskforceSource = fs.readFileSync(path.join(__dirname, 'hooks/useTaskforce.ts'), 'utf8');
|
|
5630
|
+
expect(standaloneLayoutSource).toContain("await logout();");
|
|
5631
|
+
expect(standaloneLayoutSource).toContain("if (runtimeMode === 'cloud') {");
|
|
5632
|
+
expect(standaloneLayoutSource).toContain("navigate('/login', { replace: true });");
|
|
5633
|
+
expect(useTaskforceSource).toContain('const resetCloudLogoutClientState = useCallback(() => {');
|
|
5634
|
+
expect(useTaskforceSource).toContain('setArchivedTasks([]);');
|
|
5635
|
+
expect(useTaskforceSource).toContain('setDeletedTasks([]);');
|
|
5636
|
+
expect(useTaskforceSource).toContain('setAssigneeOptions(createDefaultAssigneeOptions());');
|
|
5637
|
+
expect(useTaskforceSource).toContain('setUiStateReady(false);');
|
|
5638
|
+
expect(useTaskforceSource).toContain('resetCloudLogoutClientState();');
|
|
5639
|
+
});
|
|
3893
5640
|
it('does not pull from cloud when workspace sync toggle is disabled', async () => {
|
|
3894
5641
|
const fetchMock = vi.fn((url) => {
|
|
3895
5642
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -4758,9 +6505,10 @@ describe('TaskforceCore Integration', () => {
|
|
|
4758
6505
|
it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
|
|
4759
6506
|
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
4760
6507
|
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
4761
|
-
expect(useTaskforceSource).toContain("const
|
|
4762
|
-
expect(useTaskforceSource).toContain('Local runtime
|
|
4763
|
-
expect(useTaskforceSource).toContain('
|
|
6508
|
+
expect(useTaskforceSource).toContain("const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;");
|
|
6509
|
+
expect(useTaskforceSource).toContain('Local runtime workspace identity must be sourced from local DB (/api/taskforce/config),');
|
|
6510
|
+
expect(useTaskforceSource).toContain('not from persisted browser context or cloud session payloads.');
|
|
6511
|
+
expect(useTaskforceSource).toContain("if (initialRuntimeMode === 'local') return 'default';");
|
|
4764
6512
|
});
|
|
4765
6513
|
it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
|
|
4766
6514
|
const fetchMock = vi.fn((url, init) => {
|