@taskforcehq/taskforce 0.3.302 → 0.3.304
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- 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 +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- 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 +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- 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/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- 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 +61 -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 +122 -0
- package/dist/hooks/sync/transfers.js +431 -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 +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- 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/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/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- 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/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -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.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +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.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- 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 +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -48,6 +48,7 @@ function jsonResponse(body, status = 200) {
|
|
|
48
48
|
describe('TaskforceCore Integration', () => {
|
|
49
49
|
beforeEach(() => {
|
|
50
50
|
localStorage.clear();
|
|
51
|
+
sessionStorage.clear();
|
|
51
52
|
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
52
53
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
53
54
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
@@ -1037,7 +1038,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1037
1038
|
})
|
|
1038
1039
|
});
|
|
1039
1040
|
}
|
|
1040
|
-
if (url.includes('/api/taskforce/
|
|
1041
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1041
1042
|
return Promise.resolve({
|
|
1042
1043
|
ok: true,
|
|
1043
1044
|
json: () => Promise.resolve({
|
|
@@ -1047,7 +1048,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1047
1048
|
})
|
|
1048
1049
|
});
|
|
1049
1050
|
}
|
|
1050
|
-
if (url.includes('/api/taskforce/
|
|
1051
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1051
1052
|
return Promise.resolve({
|
|
1052
1053
|
ok: true,
|
|
1053
1054
|
json: () => Promise.resolve({
|
|
@@ -1143,7 +1144,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1143
1144
|
hasEntitlement: false,
|
|
1144
1145
|
canAccessApp: false,
|
|
1145
1146
|
canAccessPlans: true,
|
|
1146
|
-
message: '
|
|
1147
|
+
message: 'Onboarding policy is misconfigured.'
|
|
1147
1148
|
})
|
|
1148
1149
|
});
|
|
1149
1150
|
}
|
|
@@ -1196,7 +1197,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1196
1197
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
1197
1198
|
await waitFor(() => {
|
|
1198
1199
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1199
|
-
expect(screen.getByText('
|
|
1200
|
+
expect(screen.getByText('Onboarding policy is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
|
|
1200
1201
|
});
|
|
1201
1202
|
});
|
|
1202
1203
|
it('suppresses stale missing-entitlement gate messaging when billing status is already active', async () => {
|
|
@@ -1287,7 +1288,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1287
1288
|
})
|
|
1288
1289
|
});
|
|
1289
1290
|
}
|
|
1290
|
-
if (url.includes('/api/taskforce/
|
|
1291
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1291
1292
|
return Promise.resolve({
|
|
1292
1293
|
ok: true,
|
|
1293
1294
|
json: () => Promise.resolve({
|
|
@@ -1388,7 +1389,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1388
1389
|
})
|
|
1389
1390
|
});
|
|
1390
1391
|
}
|
|
1391
|
-
if (url.includes('/api/taskforce/
|
|
1392
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1392
1393
|
return Promise.resolve({
|
|
1393
1394
|
ok: true,
|
|
1394
1395
|
json: () => Promise.resolve({
|
|
@@ -1445,7 +1446,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1445
1446
|
await waitFor(() => {
|
|
1446
1447
|
expect(screen.queryByText(/Signed in as/i)).not.toBeInTheDocument();
|
|
1447
1448
|
expect(screen.getByText('member@example.com')).toBeInTheDocument();
|
|
1448
|
-
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/
|
|
1449
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/account/profile-summary'))).toBe(true);
|
|
1449
1450
|
});
|
|
1450
1451
|
await waitFor(() => {
|
|
1451
1452
|
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
@@ -1507,7 +1508,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1507
1508
|
})
|
|
1508
1509
|
});
|
|
1509
1510
|
}
|
|
1510
|
-
if (url.includes('/api/taskforce/
|
|
1511
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1511
1512
|
return Promise.resolve({
|
|
1512
1513
|
ok: true,
|
|
1513
1514
|
json: () => Promise.resolve({
|
|
@@ -1569,6 +1570,292 @@ describe('TaskforceCore Integration', () => {
|
|
|
1569
1570
|
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1570
1571
|
});
|
|
1571
1572
|
}, 10000);
|
|
1573
|
+
it('preserves the original user billing and team access after registering and switching back in local runtime', async () => {
|
|
1574
|
+
const user = userEvent.setup();
|
|
1575
|
+
const accounts = {
|
|
1576
|
+
ross: {
|
|
1577
|
+
email: 'ross.burke@gmail.com',
|
|
1578
|
+
userId: 'user-ross',
|
|
1579
|
+
displayName: 'Ross Burke',
|
|
1580
|
+
workspaceId: 'workspace-local-active',
|
|
1581
|
+
planId: 'team-5',
|
|
1582
|
+
planVersionId: 'team-5-v1',
|
|
1583
|
+
planName: 'Team 5',
|
|
1584
|
+
teamManagementAllowed: true,
|
|
1585
|
+
teamPlanMode: 'team',
|
|
1586
|
+
workspaceRole: 'owner'
|
|
1587
|
+
},
|
|
1588
|
+
newcomer: {
|
|
1589
|
+
email: 'new.user@example.com',
|
|
1590
|
+
userId: 'user-new',
|
|
1591
|
+
displayName: 'New User',
|
|
1592
|
+
workspaceId: 'workspace-local-active',
|
|
1593
|
+
planId: 'starter',
|
|
1594
|
+
planVersionId: 'starter-v1',
|
|
1595
|
+
planName: 'Starter',
|
|
1596
|
+
teamManagementAllowed: false,
|
|
1597
|
+
teamPlanMode: 'personal',
|
|
1598
|
+
workspaceRole: 'member'
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
let currentAccountKey = 'ross';
|
|
1602
|
+
const currentAccount = () => (currentAccountKey ? accounts[currentAccountKey] : null);
|
|
1603
|
+
const fetchMock = vi.fn((input, init) => {
|
|
1604
|
+
const url = String(input);
|
|
1605
|
+
const account = currentAccount();
|
|
1606
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1607
|
+
return jsonResponse({
|
|
1608
|
+
success: true,
|
|
1609
|
+
config: {
|
|
1610
|
+
baseUrl: 'https://cloud.example.com',
|
|
1611
|
+
apiBaseUrl: 'https://cloud.example.com',
|
|
1612
|
+
cloudAuthBaseUrl: 'https://cloud.example.com',
|
|
1613
|
+
runtimeMode: 'local',
|
|
1614
|
+
authSource: 'cloud',
|
|
1615
|
+
workspaceMode: 'single-local',
|
|
1616
|
+
workspaceSwitchingEnabled: false,
|
|
1617
|
+
cloudAuthViaLocalProxy: false
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1622
|
+
return jsonResponse({
|
|
1623
|
+
authenticated: Boolean(account),
|
|
1624
|
+
runtimeMode: 'local',
|
|
1625
|
+
authRequiredForApi: false,
|
|
1626
|
+
email: account?.email || '',
|
|
1627
|
+
displayName: account?.displayName || '',
|
|
1628
|
+
userId: account?.userId || 'anonymous',
|
|
1629
|
+
workspaceId: account?.workspaceId || '',
|
|
1630
|
+
betaAccess: true
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
if (url.includes('/api/taskforce/auth/logout')) {
|
|
1634
|
+
currentAccountKey = null;
|
|
1635
|
+
return jsonResponse({ success: true });
|
|
1636
|
+
}
|
|
1637
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1638
|
+
currentAccountKey = 'newcomer';
|
|
1639
|
+
return jsonResponse({
|
|
1640
|
+
success: true,
|
|
1641
|
+
workspaceSetupRequired: false,
|
|
1642
|
+
verificationRequired: false
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
1646
|
+
const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
|
|
1647
|
+
currentAccountKey = String(body?.email || '').trim().toLowerCase() === accounts.ross.email
|
|
1648
|
+
? 'ross'
|
|
1649
|
+
: 'newcomer';
|
|
1650
|
+
return jsonResponse({ success: true });
|
|
1651
|
+
}
|
|
1652
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
1653
|
+
return jsonResponse(account
|
|
1654
|
+
? {
|
|
1655
|
+
planId: account.planId,
|
|
1656
|
+
planVersionId: account.planVersionId,
|
|
1657
|
+
planName: account.planName,
|
|
1658
|
+
entitlementState: 'active',
|
|
1659
|
+
workspaceId: account.workspaceId,
|
|
1660
|
+
workspaceRole: account.workspaceRole,
|
|
1661
|
+
canManageWorkspace: account.workspaceRole === 'owner' || account.workspaceRole === 'admin',
|
|
1662
|
+
teamPlanMode: account.teamPlanMode,
|
|
1663
|
+
teamManagementAllowed: account.teamManagementAllowed
|
|
1664
|
+
}
|
|
1665
|
+
: {
|
|
1666
|
+
planId: null,
|
|
1667
|
+
planVersionId: null,
|
|
1668
|
+
planName: null,
|
|
1669
|
+
entitlementState: null,
|
|
1670
|
+
workspaceId: 'workspace-local-active',
|
|
1671
|
+
workspaceRole: null,
|
|
1672
|
+
canManageWorkspace: false,
|
|
1673
|
+
teamPlanMode: 'unknown',
|
|
1674
|
+
teamManagementAllowed: false
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
if (url.includes('/api/taskforce/auth/workspace-members')) {
|
|
1678
|
+
return jsonResponse({
|
|
1679
|
+
users: account ? [{
|
|
1680
|
+
userId: account.userId,
|
|
1681
|
+
email: account.email,
|
|
1682
|
+
displayName: account.displayName,
|
|
1683
|
+
role: account.workspaceRole,
|
|
1684
|
+
status: 'active',
|
|
1685
|
+
disabled: false
|
|
1686
|
+
}] : []
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1690
|
+
return jsonResponse({
|
|
1691
|
+
plans: [
|
|
1692
|
+
{
|
|
1693
|
+
planId: 'starter',
|
|
1694
|
+
displayName: 'Starter',
|
|
1695
|
+
status: 'active',
|
|
1696
|
+
defaultVersion: {
|
|
1697
|
+
planVersionId: 'starter-v1',
|
|
1698
|
+
versionNumber: 1,
|
|
1699
|
+
seatLimit: null,
|
|
1700
|
+
metadata: {}
|
|
1701
|
+
},
|
|
1702
|
+
features: [],
|
|
1703
|
+
pricing: {
|
|
1704
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
|
|
1705
|
+
year: null
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
planId: 'team-5',
|
|
1710
|
+
displayName: 'Team 5',
|
|
1711
|
+
status: 'active',
|
|
1712
|
+
defaultVersion: {
|
|
1713
|
+
planVersionId: 'team-5-v1',
|
|
1714
|
+
versionNumber: 1,
|
|
1715
|
+
seatLimit: 5,
|
|
1716
|
+
metadata: {}
|
|
1717
|
+
},
|
|
1718
|
+
features: [],
|
|
1719
|
+
pricing: {
|
|
1720
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 5000, currency: 'usd', interval: 'month' },
|
|
1721
|
+
year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 50000, currency: 'usd', interval: 'year' }
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
]
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
1728
|
+
return jsonResponse({ success: true, state: {} });
|
|
1729
|
+
}
|
|
1730
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
1731
|
+
return jsonResponse({
|
|
1732
|
+
success: true,
|
|
1733
|
+
state: {
|
|
1734
|
+
version: 2,
|
|
1735
|
+
enabled: false,
|
|
1736
|
+
phase: 'idle',
|
|
1737
|
+
pullCursor: null,
|
|
1738
|
+
lastPullAt: null,
|
|
1739
|
+
lastPushAt: null,
|
|
1740
|
+
lastSyncedAt: null
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1745
|
+
return jsonResponse({
|
|
1746
|
+
...mockConfig,
|
|
1747
|
+
runtimeMode: 'local',
|
|
1748
|
+
authRequiredForApi: false,
|
|
1749
|
+
workspaceId: 'workspace-local-active',
|
|
1750
|
+
setupState: {
|
|
1751
|
+
globalSetupState: 'present',
|
|
1752
|
+
workspaceSetupState: 'present',
|
|
1753
|
+
runtimeMode: 'local',
|
|
1754
|
+
workspaceId: 'workspace-local-active',
|
|
1755
|
+
mode: 'core',
|
|
1756
|
+
forceSetup: false,
|
|
1757
|
+
forceGlobalSetup: false,
|
|
1758
|
+
forceWorkspaceSetup: false
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
1763
|
+
return jsonResponse({ tasks: mockTasks });
|
|
1764
|
+
}
|
|
1765
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
1766
|
+
return jsonResponse({ archived: [] });
|
|
1767
|
+
}
|
|
1768
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
1769
|
+
return jsonResponse(mockCategories);
|
|
1770
|
+
}
|
|
1771
|
+
if (url.includes('/api/taskforce/types')) {
|
|
1772
|
+
return jsonResponse({ types: [] });
|
|
1773
|
+
}
|
|
1774
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
1775
|
+
return jsonResponse({ priorities: [] });
|
|
1776
|
+
}
|
|
1777
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
1778
|
+
return jsonResponse({ approaches: [] });
|
|
1779
|
+
}
|
|
1780
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
1781
|
+
return jsonResponse({ specialists: [] });
|
|
1782
|
+
}
|
|
1783
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1784
|
+
return jsonResponse({ templates: [] });
|
|
1785
|
+
}
|
|
1786
|
+
if (url.includes('/api/taskforce/workspace/assignee-options')) {
|
|
1787
|
+
return jsonResponse({ assignees: [] });
|
|
1788
|
+
}
|
|
1789
|
+
if (url.includes('/api/taskforce/initiatives')) {
|
|
1790
|
+
return jsonResponse([]);
|
|
1791
|
+
}
|
|
1792
|
+
if (url.includes('/api/taskforce/workstreams')) {
|
|
1793
|
+
return jsonResponse([]);
|
|
1794
|
+
}
|
|
1795
|
+
return jsonResponse({});
|
|
1796
|
+
});
|
|
1797
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1798
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1799
|
+
await waitFor(() => {
|
|
1800
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1801
|
+
});
|
|
1802
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
1803
|
+
await waitFor(() => {
|
|
1804
|
+
expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
|
|
1805
|
+
expect(screen.getByText('Team 5')).toBeInTheDocument();
|
|
1806
|
+
expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
|
|
1807
|
+
});
|
|
1808
|
+
await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
|
|
1809
|
+
await waitFor(() => {
|
|
1810
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1811
|
+
});
|
|
1812
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
1813
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
|
|
1814
|
+
await waitFor(() => {
|
|
1815
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
1816
|
+
});
|
|
1817
|
+
await user.click(screen.getByRole('button', { name: 'Register' }));
|
|
1818
|
+
await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
|
|
1819
|
+
await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
|
|
1820
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1821
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1822
|
+
await waitFor(() => {
|
|
1823
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1824
|
+
});
|
|
1825
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
1826
|
+
await waitFor(() => {
|
|
1827
|
+
expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
|
|
1828
|
+
expect(screen.getByText('Starter')).toBeInTheDocument();
|
|
1829
|
+
});
|
|
1830
|
+
await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
|
|
1831
|
+
await waitFor(() => {
|
|
1832
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1833
|
+
});
|
|
1834
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
1835
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
|
|
1836
|
+
await waitFor(() => {
|
|
1837
|
+
expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
|
|
1838
|
+
});
|
|
1839
|
+
await user.type(await screen.findByPlaceholderText('Email'), accounts.ross.email);
|
|
1840
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1841
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
1842
|
+
await waitFor(() => {
|
|
1843
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1844
|
+
});
|
|
1845
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
1846
|
+
await waitFor(() => {
|
|
1847
|
+
expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
|
|
1848
|
+
expect(screen.getByText('Team 5')).toBeInTheDocument();
|
|
1849
|
+
expect(screen.queryByText('Starter')).not.toBeInTheDocument();
|
|
1850
|
+
expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
|
|
1851
|
+
});
|
|
1852
|
+
await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
|
|
1853
|
+
await waitFor(() => {
|
|
1854
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1855
|
+
});
|
|
1856
|
+
const currentPlanCard = screen.getByTestId('plan-card-team-5');
|
|
1857
|
+
expect(within(currentPlanCard).getByTestId('plan-manage-team-5')).toHaveTextContent('Current Plan');
|
|
1858
|
+
});
|
|
1572
1859
|
it('allows local runtime to open the shared cloud-backed register route', async () => {
|
|
1573
1860
|
const fetchMock = vi.fn((url) => {
|
|
1574
1861
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -1582,44 +1869,995 @@ describe('TaskforceCore Integration', () => {
|
|
|
1582
1869
|
});
|
|
1583
1870
|
}
|
|
1584
1871
|
if (url.includes('/api/taskforce/config')) {
|
|
1585
|
-
return Promise.resolve({
|
|
1586
|
-
ok: true,
|
|
1587
|
-
json: () => Promise.resolve({
|
|
1588
|
-
...mockConfig,
|
|
1589
|
-
runtimeMode: 'local',
|
|
1590
|
-
authRequiredForApi: false,
|
|
1591
|
-
setupState: {
|
|
1592
|
-
globalSetupState: 'present',
|
|
1593
|
-
workspaceSetupState: 'present',
|
|
1594
|
-
runtimeMode: 'local',
|
|
1595
|
-
workspaceId: 'workspace-local-active',
|
|
1596
|
-
mode: 'core',
|
|
1597
|
-
forceSetup: false,
|
|
1598
|
-
forceGlobalSetup: false,
|
|
1599
|
-
forceWorkspaceSetup: false
|
|
1600
|
-
}
|
|
1601
|
-
})
|
|
1872
|
+
return Promise.resolve({
|
|
1873
|
+
ok: true,
|
|
1874
|
+
json: () => Promise.resolve({
|
|
1875
|
+
...mockConfig,
|
|
1876
|
+
runtimeMode: 'local',
|
|
1877
|
+
authRequiredForApi: false,
|
|
1878
|
+
setupState: {
|
|
1879
|
+
globalSetupState: 'present',
|
|
1880
|
+
workspaceSetupState: 'present',
|
|
1881
|
+
runtimeMode: 'local',
|
|
1882
|
+
workspaceId: 'workspace-local-active',
|
|
1883
|
+
mode: 'core',
|
|
1884
|
+
forceSetup: false,
|
|
1885
|
+
forceGlobalSetup: false,
|
|
1886
|
+
forceWorkspaceSetup: false
|
|
1887
|
+
}
|
|
1888
|
+
})
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1892
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1893
|
+
}
|
|
1894
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1895
|
+
});
|
|
1896
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1897
|
+
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1898
|
+
await waitFor(() => {
|
|
1899
|
+
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
1900
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
1901
|
+
expect(screen.getByLabelText('Display name')).toBeInTheDocument();
|
|
1902
|
+
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1903
|
+
expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
|
|
1904
|
+
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
1905
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
1906
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1907
|
+
expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
|
|
1908
|
+
expect(screen.getByTestId('auth-submit-button')).toBeInTheDocument();
|
|
1909
|
+
expect(document.title).toBe('Create Account | Taskforce');
|
|
1910
|
+
});
|
|
1911
|
+
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
1912
|
+
});
|
|
1913
|
+
it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
|
|
1914
|
+
const fetchMock = vi.fn((url) => {
|
|
1915
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1916
|
+
return Promise.resolve({
|
|
1917
|
+
ok: true,
|
|
1918
|
+
json: () => Promise.resolve({
|
|
1919
|
+
authenticated: false,
|
|
1920
|
+
runtimeMode: 'cloud',
|
|
1921
|
+
authRequiredForApi: false
|
|
1922
|
+
})
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1926
|
+
return Promise.resolve({
|
|
1927
|
+
ok: true,
|
|
1928
|
+
json: () => Promise.resolve({
|
|
1929
|
+
...mockConfig,
|
|
1930
|
+
runtimeMode: 'cloud',
|
|
1931
|
+
authRequiredForApi: false,
|
|
1932
|
+
setupState: {
|
|
1933
|
+
globalSetupState: 'present',
|
|
1934
|
+
workspaceSetupState: 'present',
|
|
1935
|
+
runtimeMode: 'cloud',
|
|
1936
|
+
workspaceId: 'workspace-local-active',
|
|
1937
|
+
mode: 'core',
|
|
1938
|
+
forceSetup: false,
|
|
1939
|
+
forceGlobalSetup: false,
|
|
1940
|
+
forceWorkspaceSetup: false
|
|
1941
|
+
}
|
|
1942
|
+
})
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1946
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1947
|
+
}
|
|
1948
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1949
|
+
});
|
|
1950
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1951
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
1952
|
+
await waitFor(() => {
|
|
1953
|
+
expect(screen.getByText('Enter your verification token.')).toBeInTheDocument();
|
|
1954
|
+
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1955
|
+
expect(screen.getByRole('button', { name: 'Verify Email' })).toBeInTheDocument();
|
|
1956
|
+
expect(screen.getByRole('button', { name: 'Back to Sign In' })).toBeInTheDocument();
|
|
1957
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1958
|
+
expect(screen.getByRole('button', { name: 'Resend Verification' })).toBeDisabled();
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1961
|
+
it('allows the verify screen to resend a verification email after the user re-enters their email', async () => {
|
|
1962
|
+
const user = userEvent.setup();
|
|
1963
|
+
const fetchMock = vi.fn((input) => {
|
|
1964
|
+
const url = String(input);
|
|
1965
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1966
|
+
return Promise.resolve({
|
|
1967
|
+
ok: true,
|
|
1968
|
+
json: () => Promise.resolve({
|
|
1969
|
+
authenticated: false,
|
|
1970
|
+
runtimeMode: 'cloud',
|
|
1971
|
+
authRequiredForApi: false
|
|
1972
|
+
})
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1976
|
+
return Promise.resolve({
|
|
1977
|
+
ok: true,
|
|
1978
|
+
json: () => Promise.resolve({
|
|
1979
|
+
...mockConfig,
|
|
1980
|
+
runtimeMode: 'cloud',
|
|
1981
|
+
authRequiredForApi: false,
|
|
1982
|
+
setupState: {
|
|
1983
|
+
globalSetupState: 'present',
|
|
1984
|
+
workspaceSetupState: 'present',
|
|
1985
|
+
runtimeMode: 'cloud',
|
|
1986
|
+
workspaceId: 'workspace-local-active',
|
|
1987
|
+
mode: 'core',
|
|
1988
|
+
forceSetup: false,
|
|
1989
|
+
forceGlobalSetup: false,
|
|
1990
|
+
forceWorkspaceSetup: false
|
|
1991
|
+
}
|
|
1992
|
+
})
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1996
|
+
return Promise.resolve({
|
|
1997
|
+
ok: true,
|
|
1998
|
+
json: () => Promise.resolve({
|
|
1999
|
+
success: true,
|
|
2000
|
+
verificationRequired: true,
|
|
2001
|
+
workspaceSetupRequired: false,
|
|
2002
|
+
emailSent: true
|
|
2003
|
+
})
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
if (url.includes('/api/taskforce/auth/verify-email/request')) {
|
|
2007
|
+
return Promise.resolve({
|
|
2008
|
+
ok: true,
|
|
2009
|
+
json: () => Promise.resolve({
|
|
2010
|
+
success: true,
|
|
2011
|
+
verificationToken: 'resent-token'
|
|
2012
|
+
})
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2016
|
+
return Promise.resolve({
|
|
2017
|
+
ok: true,
|
|
2018
|
+
json: () => Promise.resolve({
|
|
2019
|
+
plans: []
|
|
2020
|
+
})
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2024
|
+
});
|
|
2025
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2026
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
2027
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
|
|
2028
|
+
await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
|
|
2029
|
+
await waitFor(() => {
|
|
2030
|
+
expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeInTheDocument();
|
|
2031
|
+
expect(screen.getByDisplayValue('resent-token')).toBeInTheDocument();
|
|
2032
|
+
expect(screen.getByText('Verification email resent.')).toBeInTheDocument();
|
|
2033
|
+
});
|
|
2034
|
+
expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
|
|
2035
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
|
|
2036
|
+
});
|
|
2037
|
+
it('does not switch to verify mode after registration when verification is not required', async () => {
|
|
2038
|
+
const user = userEvent.setup();
|
|
2039
|
+
let authenticated = false;
|
|
2040
|
+
const fetchMock = vi.fn((input) => {
|
|
2041
|
+
const url = String(input);
|
|
2042
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2043
|
+
return Promise.resolve({
|
|
2044
|
+
ok: true,
|
|
2045
|
+
json: () => Promise.resolve({
|
|
2046
|
+
config: {
|
|
2047
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2048
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2049
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2050
|
+
runtimeMode: 'cloud',
|
|
2051
|
+
authSource: 'cloud',
|
|
2052
|
+
workspaceMode: 'multi-cloud',
|
|
2053
|
+
workspaceSwitchingEnabled: true,
|
|
2054
|
+
authRequiredForApi: true
|
|
2055
|
+
}
|
|
2056
|
+
})
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2060
|
+
return Promise.resolve({
|
|
2061
|
+
ok: true,
|
|
2062
|
+
json: () => Promise.resolve({
|
|
2063
|
+
authenticated,
|
|
2064
|
+
runtimeMode: 'cloud',
|
|
2065
|
+
authRequiredForApi: true,
|
|
2066
|
+
userId: authenticated ? 'user-no-verify' : null,
|
|
2067
|
+
workspaceId: null,
|
|
2068
|
+
betaAccess: true
|
|
2069
|
+
})
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2073
|
+
return Promise.resolve({
|
|
2074
|
+
ok: true,
|
|
2075
|
+
json: () => Promise.resolve({
|
|
2076
|
+
...mockConfig,
|
|
2077
|
+
runtimeMode: 'cloud',
|
|
2078
|
+
authRequiredForApi: true,
|
|
2079
|
+
workspaceSwitchingEnabled: true,
|
|
2080
|
+
setupState: {
|
|
2081
|
+
globalSetupState: 'present',
|
|
2082
|
+
workspaceSetupState: 'missing',
|
|
2083
|
+
runtimeMode: 'cloud',
|
|
2084
|
+
workspaceId: 'default',
|
|
2085
|
+
mode: 'core',
|
|
2086
|
+
forceSetup: true,
|
|
2087
|
+
forceGlobalSetup: false,
|
|
2088
|
+
forceWorkspaceSetup: true
|
|
2089
|
+
}
|
|
2090
|
+
})
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2094
|
+
return Promise.resolve({
|
|
2095
|
+
ok: true,
|
|
2096
|
+
json: () => Promise.resolve({
|
|
2097
|
+
success: true,
|
|
2098
|
+
workspaces: [],
|
|
2099
|
+
currentWorkspaceId: 'default'
|
|
2100
|
+
})
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2104
|
+
return Promise.resolve({
|
|
2105
|
+
ok: true,
|
|
2106
|
+
json: () => Promise.resolve({
|
|
2107
|
+
gate: 'ok',
|
|
2108
|
+
hasEntitlement: true,
|
|
2109
|
+
canAccessApp: true,
|
|
2110
|
+
canAccessPlans: true,
|
|
2111
|
+
message: null
|
|
2112
|
+
})
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2116
|
+
return Promise.resolve({
|
|
2117
|
+
ok: true,
|
|
2118
|
+
json: () => Promise.resolve({
|
|
2119
|
+
plans: []
|
|
2120
|
+
})
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2124
|
+
authenticated = true;
|
|
2125
|
+
return Promise.resolve({
|
|
2126
|
+
ok: true,
|
|
2127
|
+
json: () => Promise.resolve({
|
|
2128
|
+
success: true,
|
|
2129
|
+
workspaceSetupRequired: true,
|
|
2130
|
+
verificationRequired: false,
|
|
2131
|
+
commercialState: 'active'
|
|
2132
|
+
})
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2136
|
+
});
|
|
2137
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2138
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
2139
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
2140
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
2141
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2142
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2143
|
+
await waitFor(() => {
|
|
2144
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2145
|
+
});
|
|
2146
|
+
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
2147
|
+
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
2148
|
+
});
|
|
2149
|
+
it('routes successful registration into plan selection when onboarding leaves the account pending plan selection', async () => {
|
|
2150
|
+
const user = userEvent.setup();
|
|
2151
|
+
let authenticated = false;
|
|
2152
|
+
const fetchMock = vi.fn((input) => {
|
|
2153
|
+
const url = String(input);
|
|
2154
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2155
|
+
return jsonResponse({
|
|
2156
|
+
config: {
|
|
2157
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2158
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2159
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2160
|
+
runtimeMode: 'cloud',
|
|
2161
|
+
authSource: 'cloud',
|
|
2162
|
+
workspaceMode: 'multi-cloud',
|
|
2163
|
+
workspaceSwitchingEnabled: true,
|
|
2164
|
+
authRequiredForApi: true
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2169
|
+
return jsonResponse({
|
|
2170
|
+
authenticated,
|
|
2171
|
+
runtimeMode: 'cloud',
|
|
2172
|
+
authRequiredForApi: true,
|
|
2173
|
+
userId: authenticated ? 'user-1' : null,
|
|
2174
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2175
|
+
betaAccess: true
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2179
|
+
return jsonResponse({
|
|
2180
|
+
...mockConfig,
|
|
2181
|
+
runtimeMode: 'cloud',
|
|
2182
|
+
authRequiredForApi: true,
|
|
2183
|
+
workspaceSwitchingEnabled: true,
|
|
2184
|
+
setupState: {
|
|
2185
|
+
globalSetupState: 'present',
|
|
2186
|
+
workspaceSetupState: 'present',
|
|
2187
|
+
runtimeMode: 'cloud',
|
|
2188
|
+
workspaceId: 'workspace-1',
|
|
2189
|
+
mode: 'core',
|
|
2190
|
+
forceSetup: false,
|
|
2191
|
+
forceGlobalSetup: false,
|
|
2192
|
+
forceWorkspaceSetup: false
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2197
|
+
return jsonResponse({
|
|
2198
|
+
success: true,
|
|
2199
|
+
workspaces: [{
|
|
2200
|
+
id: 'workspace-1',
|
|
2201
|
+
slug: 'workspace-1',
|
|
2202
|
+
name: 'Workspace 1',
|
|
2203
|
+
description: null,
|
|
2204
|
+
role: 'owner',
|
|
2205
|
+
status: 'active',
|
|
2206
|
+
betaAccess: true
|
|
2207
|
+
}],
|
|
2208
|
+
currentWorkspaceId: 'workspace-1'
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2212
|
+
return jsonResponse({
|
|
2213
|
+
gate: 'plan_selection_required',
|
|
2214
|
+
hasEntitlement: true,
|
|
2215
|
+
canAccessApp: false,
|
|
2216
|
+
canAccessPlans: true,
|
|
2217
|
+
message: 'Choose a plan to continue.'
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2221
|
+
return jsonResponse({
|
|
2222
|
+
entitlementState: 'pending_plan_selection',
|
|
2223
|
+
gate: 'plan_selection_required',
|
|
2224
|
+
message: 'Choose a plan to continue.',
|
|
2225
|
+
workspaceId: 'workspace-1'
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2229
|
+
return jsonResponse({
|
|
2230
|
+
plans: [{
|
|
2231
|
+
planId: 'starter',
|
|
2232
|
+
displayName: 'Starter',
|
|
2233
|
+
status: 'active',
|
|
2234
|
+
defaultVersion: {
|
|
2235
|
+
planVersionId: 'starter-v1',
|
|
2236
|
+
versionNumber: 1,
|
|
2237
|
+
seatLimit: null,
|
|
2238
|
+
metadata: {}
|
|
2239
|
+
},
|
|
2240
|
+
features: [],
|
|
2241
|
+
pricing: {
|
|
2242
|
+
month: {
|
|
2243
|
+
planVersionId: 'starter-v1',
|
|
2244
|
+
interval: 'month',
|
|
2245
|
+
pricingType: 'free',
|
|
2246
|
+
stripePriceId: 'free:starter-v1:month',
|
|
2247
|
+
displayAmount: '$0.00',
|
|
2248
|
+
unitAmount: 0,
|
|
2249
|
+
currency: 'usd'
|
|
2250
|
+
},
|
|
2251
|
+
year: null
|
|
2252
|
+
}
|
|
2253
|
+
}]
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2257
|
+
authenticated = true;
|
|
2258
|
+
return jsonResponse({
|
|
2259
|
+
success: true,
|
|
2260
|
+
workspaceSetupRequired: false,
|
|
2261
|
+
verificationRequired: false,
|
|
2262
|
+
planSelectionRequired: true,
|
|
2263
|
+
commercialState: 'pending_plan_selection'
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
2267
|
+
return jsonResponse(mockCategories);
|
|
2268
|
+
}
|
|
2269
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2270
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2271
|
+
}
|
|
2272
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
2273
|
+
return jsonResponse({ archived: [] });
|
|
2274
|
+
}
|
|
2275
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
2276
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2277
|
+
}
|
|
2278
|
+
if (url.includes('/api/taskforce/types')) {
|
|
2279
|
+
return jsonResponse({ types: [] });
|
|
2280
|
+
}
|
|
2281
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
2282
|
+
return jsonResponse({ priorities: [] });
|
|
2283
|
+
}
|
|
2284
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
2285
|
+
return jsonResponse({ approaches: [] });
|
|
2286
|
+
}
|
|
2287
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
2288
|
+
return jsonResponse({ specialists: [] });
|
|
2289
|
+
}
|
|
2290
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2291
|
+
return jsonResponse({ templates: [] });
|
|
2292
|
+
}
|
|
2293
|
+
return jsonResponse({});
|
|
2294
|
+
});
|
|
2295
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2296
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2297
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
|
|
2298
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
|
|
2299
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2300
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2301
|
+
await waitFor(() => {
|
|
2302
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2303
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
2304
|
+
});
|
|
2305
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
|
|
2306
|
+
});
|
|
2307
|
+
it('keeps registration on plan selection when commercial onboarding is blocked even if the new cloud user has no workspace yet', async () => {
|
|
2308
|
+
const user = userEvent.setup();
|
|
2309
|
+
let authenticated = false;
|
|
2310
|
+
const fetchMock = vi.fn((input) => {
|
|
2311
|
+
const url = String(input);
|
|
2312
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2313
|
+
return jsonResponse({
|
|
2314
|
+
config: {
|
|
2315
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2316
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2317
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2318
|
+
runtimeMode: 'cloud',
|
|
2319
|
+
authSource: 'cloud',
|
|
2320
|
+
workspaceMode: 'multi-cloud',
|
|
2321
|
+
workspaceSwitchingEnabled: true,
|
|
2322
|
+
authRequiredForApi: true
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2327
|
+
return jsonResponse({
|
|
2328
|
+
authenticated,
|
|
2329
|
+
runtimeMode: 'cloud',
|
|
2330
|
+
authRequiredForApi: true,
|
|
2331
|
+
userId: authenticated ? 'user-new' : null,
|
|
2332
|
+
workspaceId: null,
|
|
2333
|
+
betaAccess: true
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2337
|
+
return jsonResponse({
|
|
2338
|
+
...mockConfig,
|
|
2339
|
+
runtimeMode: 'cloud',
|
|
2340
|
+
authRequiredForApi: true,
|
|
2341
|
+
workspaceSwitchingEnabled: true,
|
|
2342
|
+
setupState: {
|
|
2343
|
+
globalSetupState: 'present',
|
|
2344
|
+
workspaceSetupState: 'missing',
|
|
2345
|
+
runtimeMode: 'cloud',
|
|
2346
|
+
workspaceId: 'default',
|
|
2347
|
+
mode: 'core',
|
|
2348
|
+
forceSetup: false,
|
|
2349
|
+
forceGlobalSetup: false,
|
|
2350
|
+
forceWorkspaceSetup: false
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2355
|
+
return jsonResponse({
|
|
2356
|
+
success: true,
|
|
2357
|
+
workspaces: [],
|
|
2358
|
+
currentWorkspaceId: 'default'
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2362
|
+
return jsonResponse({
|
|
2363
|
+
gate: 'plan_selection_required',
|
|
2364
|
+
hasEntitlement: true,
|
|
2365
|
+
canAccessApp: false,
|
|
2366
|
+
canAccessPlans: true,
|
|
2367
|
+
message: 'Choose a plan to continue.'
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2371
|
+
return jsonResponse({
|
|
2372
|
+
entitlementState: 'pending_plan_selection',
|
|
2373
|
+
gate: 'plan_selection_required',
|
|
2374
|
+
message: 'Choose a plan to continue.',
|
|
2375
|
+
workspaceId: null
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2379
|
+
return jsonResponse({ plans: [] });
|
|
2380
|
+
}
|
|
2381
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2382
|
+
authenticated = true;
|
|
2383
|
+
return jsonResponse({
|
|
2384
|
+
success: true,
|
|
2385
|
+
workspaceSetupRequired: true,
|
|
2386
|
+
verificationRequired: false,
|
|
2387
|
+
planSelectionRequired: true,
|
|
2388
|
+
commercialState: 'pending_plan_selection'
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2392
|
+
return jsonResponse(mockCategories);
|
|
2393
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2394
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2395
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2396
|
+
return jsonResponse({ archived: [] });
|
|
2397
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2398
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2399
|
+
if (url.includes('/api/taskforce/types'))
|
|
2400
|
+
return jsonResponse({ types: [] });
|
|
2401
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2402
|
+
return jsonResponse({ priorities: [] });
|
|
2403
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2404
|
+
return jsonResponse({ approaches: [] });
|
|
2405
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2406
|
+
return jsonResponse({ specialists: [] });
|
|
2407
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2408
|
+
return jsonResponse({ templates: [] });
|
|
2409
|
+
return jsonResponse({});
|
|
2410
|
+
});
|
|
2411
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2412
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2413
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
|
|
2414
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
|
|
2415
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2416
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2417
|
+
await waitFor(() => {
|
|
2418
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2419
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
2420
|
+
});
|
|
2421
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2422
|
+
});
|
|
2423
|
+
it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
|
|
2424
|
+
const user = userEvent.setup();
|
|
2425
|
+
let authenticated = false;
|
|
2426
|
+
const fetchMock = vi.fn((input) => {
|
|
2427
|
+
const url = String(input);
|
|
2428
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2429
|
+
return jsonResponse({
|
|
2430
|
+
config: {
|
|
2431
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2432
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2433
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2434
|
+
runtimeMode: 'cloud',
|
|
2435
|
+
authSource: 'cloud',
|
|
2436
|
+
workspaceMode: 'multi-cloud',
|
|
2437
|
+
workspaceSwitchingEnabled: true,
|
|
2438
|
+
authRequiredForApi: true
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2443
|
+
return jsonResponse({
|
|
2444
|
+
authenticated,
|
|
2445
|
+
runtimeMode: 'cloud',
|
|
2446
|
+
authRequiredForApi: true,
|
|
2447
|
+
userId: authenticated ? 'user-1' : null,
|
|
2448
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2449
|
+
betaAccess: true
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2453
|
+
return jsonResponse({
|
|
2454
|
+
...mockConfig,
|
|
2455
|
+
runtimeMode: 'cloud',
|
|
2456
|
+
authRequiredForApi: true,
|
|
2457
|
+
workspaceSwitchingEnabled: true,
|
|
2458
|
+
setupState: {
|
|
2459
|
+
globalSetupState: 'present',
|
|
2460
|
+
workspaceSetupState: 'present',
|
|
2461
|
+
runtimeMode: 'cloud',
|
|
2462
|
+
workspaceId: 'workspace-1',
|
|
2463
|
+
mode: 'core',
|
|
2464
|
+
forceSetup: false,
|
|
2465
|
+
forceGlobalSetup: false,
|
|
2466
|
+
forceWorkspaceSetup: false
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2471
|
+
return jsonResponse({
|
|
2472
|
+
success: true,
|
|
2473
|
+
workspaces: [{
|
|
2474
|
+
id: 'workspace-1',
|
|
2475
|
+
slug: 'workspace-1',
|
|
2476
|
+
name: 'Workspace 1',
|
|
2477
|
+
description: null,
|
|
2478
|
+
role: 'owner',
|
|
2479
|
+
status: 'active',
|
|
2480
|
+
betaAccess: true
|
|
2481
|
+
}],
|
|
2482
|
+
currentWorkspaceId: 'workspace-1'
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2486
|
+
return jsonResponse({
|
|
2487
|
+
gate: 'checkout_pending',
|
|
2488
|
+
hasEntitlement: true,
|
|
2489
|
+
canAccessApp: false,
|
|
2490
|
+
canAccessPlans: true,
|
|
2491
|
+
message: 'Complete checkout to continue.'
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2495
|
+
return jsonResponse({
|
|
2496
|
+
planId: 'pro',
|
|
2497
|
+
planName: 'Pro',
|
|
2498
|
+
planVersionId: 'pro-v1',
|
|
2499
|
+
entitlementState: 'checkout_pending',
|
|
2500
|
+
gate: 'checkout_pending',
|
|
2501
|
+
message: 'Complete checkout to continue.',
|
|
2502
|
+
workspaceId: 'workspace-1'
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2506
|
+
return jsonResponse({
|
|
2507
|
+
plans: [{
|
|
2508
|
+
planId: 'pro',
|
|
2509
|
+
displayName: 'Pro',
|
|
2510
|
+
status: 'active',
|
|
2511
|
+
defaultVersion: {
|
|
2512
|
+
planVersionId: 'pro-v1',
|
|
2513
|
+
versionNumber: 1,
|
|
2514
|
+
seatLimit: null,
|
|
2515
|
+
metadata: {}
|
|
2516
|
+
},
|
|
2517
|
+
features: [],
|
|
2518
|
+
pricing: {
|
|
2519
|
+
month: {
|
|
2520
|
+
planVersionId: 'pro-v1',
|
|
2521
|
+
interval: 'month',
|
|
2522
|
+
pricingType: 'stripe',
|
|
2523
|
+
stripePriceId: 'price_pro_month',
|
|
2524
|
+
displayAmount: '$19.00',
|
|
2525
|
+
unitAmount: 1900,
|
|
2526
|
+
currency: 'usd'
|
|
2527
|
+
},
|
|
2528
|
+
year: {
|
|
2529
|
+
planVersionId: 'pro-v1',
|
|
2530
|
+
interval: 'year',
|
|
2531
|
+
pricingType: 'stripe',
|
|
2532
|
+
stripePriceId: 'price_pro_year',
|
|
2533
|
+
displayAmount: '$190.00',
|
|
2534
|
+
unitAmount: 19000,
|
|
2535
|
+
currency: 'usd'
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}]
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2542
|
+
authenticated = true;
|
|
2543
|
+
return jsonResponse({
|
|
2544
|
+
success: true,
|
|
2545
|
+
workspaceSetupRequired: false,
|
|
2546
|
+
verificationRequired: false,
|
|
2547
|
+
checkoutPending: true,
|
|
2548
|
+
commercialState: 'checkout_pending'
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
if (url.includes('/api/taskforce/billing/checkout-session')) {
|
|
2552
|
+
return jsonResponse({
|
|
2553
|
+
checkoutState: 'updated',
|
|
2554
|
+
message: 'Checkout started.'
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
2558
|
+
return jsonResponse(mockCategories);
|
|
2559
|
+
}
|
|
2560
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2561
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2562
|
+
}
|
|
2563
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
2564
|
+
return jsonResponse({ archived: [] });
|
|
2565
|
+
}
|
|
2566
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
2567
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2568
|
+
}
|
|
2569
|
+
if (url.includes('/api/taskforce/types')) {
|
|
2570
|
+
return jsonResponse({ types: [] });
|
|
2571
|
+
}
|
|
2572
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
2573
|
+
return jsonResponse({ priorities: [] });
|
|
2574
|
+
}
|
|
2575
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
2576
|
+
return jsonResponse({ approaches: [] });
|
|
2577
|
+
}
|
|
2578
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
2579
|
+
return jsonResponse({ specialists: [] });
|
|
2580
|
+
}
|
|
2581
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2582
|
+
return jsonResponse({ templates: [] });
|
|
2583
|
+
}
|
|
2584
|
+
return jsonResponse({});
|
|
2585
|
+
});
|
|
2586
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2587
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2588
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
|
|
2589
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
|
|
2590
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2591
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2592
|
+
await waitFor(() => {
|
|
2593
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2594
|
+
});
|
|
2595
|
+
await waitFor(() => {
|
|
2596
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
|
|
2597
|
+
});
|
|
2598
|
+
});
|
|
2599
|
+
it('routes successful registration into workspace setup when onboarding auto-grants free, even if a stale pre-registration session check resolves later', async () => {
|
|
2600
|
+
const user = userEvent.setup();
|
|
2601
|
+
let authenticated = false;
|
|
2602
|
+
let authSessionCallCount = 0;
|
|
2603
|
+
let releaseInitialSession = null;
|
|
2604
|
+
const initialSessionGate = new Promise((resolve) => {
|
|
2605
|
+
releaseInitialSession = resolve;
|
|
2606
|
+
});
|
|
2607
|
+
const fetchMock = vi.fn((input) => {
|
|
2608
|
+
const url = String(input);
|
|
2609
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2610
|
+
return jsonResponse({
|
|
2611
|
+
config: {
|
|
2612
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2613
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2614
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2615
|
+
runtimeMode: 'cloud',
|
|
2616
|
+
authSource: 'cloud',
|
|
2617
|
+
workspaceMode: 'multi-cloud',
|
|
2618
|
+
workspaceSwitchingEnabled: true,
|
|
2619
|
+
authRequiredForApi: true
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2624
|
+
authSessionCallCount += 1;
|
|
2625
|
+
const payload = () => ({
|
|
2626
|
+
authenticated,
|
|
2627
|
+
runtimeMode: 'cloud',
|
|
2628
|
+
authRequiredForApi: true,
|
|
2629
|
+
userId: authenticated ? 'user-free-1' : null,
|
|
2630
|
+
workspaceId: null,
|
|
2631
|
+
betaAccess: true
|
|
2632
|
+
});
|
|
2633
|
+
if (authSessionCallCount === 1) {
|
|
2634
|
+
return initialSessionGate.then(() => jsonResponse(payload()));
|
|
2635
|
+
}
|
|
2636
|
+
return jsonResponse(payload());
|
|
2637
|
+
}
|
|
2638
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2639
|
+
return jsonResponse({
|
|
2640
|
+
...mockConfig,
|
|
2641
|
+
runtimeMode: 'cloud',
|
|
2642
|
+
authRequiredForApi: true,
|
|
2643
|
+
workspaceSwitchingEnabled: true,
|
|
2644
|
+
setupState: {
|
|
2645
|
+
globalSetupState: 'present',
|
|
2646
|
+
workspaceSetupState: 'missing',
|
|
2647
|
+
runtimeMode: 'cloud',
|
|
2648
|
+
workspaceId: 'default',
|
|
2649
|
+
mode: 'core',
|
|
2650
|
+
forceSetup: true,
|
|
2651
|
+
forceGlobalSetup: false,
|
|
2652
|
+
forceWorkspaceSetup: true
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2657
|
+
return jsonResponse({
|
|
2658
|
+
success: true,
|
|
2659
|
+
workspaces: [],
|
|
2660
|
+
currentWorkspaceId: 'default'
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2664
|
+
return jsonResponse({
|
|
2665
|
+
gate: 'ok',
|
|
2666
|
+
hasEntitlement: true,
|
|
2667
|
+
canAccessApp: true,
|
|
2668
|
+
canAccessPlans: true,
|
|
2669
|
+
message: null
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2673
|
+
return jsonResponse({ plans: [] });
|
|
2674
|
+
}
|
|
2675
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2676
|
+
authenticated = true;
|
|
2677
|
+
return jsonResponse({
|
|
2678
|
+
success: true,
|
|
2679
|
+
workspaceSetupRequired: true,
|
|
2680
|
+
verificationRequired: false,
|
|
2681
|
+
commercialState: 'active'
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2685
|
+
return jsonResponse(mockCategories);
|
|
2686
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2687
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2688
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2689
|
+
return jsonResponse({ archived: [] });
|
|
2690
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2691
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2692
|
+
if (url.includes('/api/taskforce/types'))
|
|
2693
|
+
return jsonResponse({ types: [] });
|
|
2694
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2695
|
+
return jsonResponse({ priorities: [] });
|
|
2696
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2697
|
+
return jsonResponse({ approaches: [] });
|
|
2698
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2699
|
+
return jsonResponse({ specialists: [] });
|
|
2700
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2701
|
+
return jsonResponse({ templates: [] });
|
|
2702
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
2703
|
+
return jsonResponse({
|
|
2704
|
+
packs: [{
|
|
2705
|
+
id: 'general',
|
|
2706
|
+
label: 'General',
|
|
2707
|
+
description: 'Starter',
|
|
2708
|
+
isDefaultStarter: true,
|
|
2709
|
+
sections: {
|
|
2710
|
+
categories: [{ value: 'general', label: 'General' }],
|
|
2711
|
+
taskTypes: [{ value: 'task', label: 'Task' }],
|
|
2712
|
+
priorities: [{ value: 1, label: 'Low' }]
|
|
2713
|
+
}
|
|
2714
|
+
}]
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
return jsonResponse({});
|
|
2718
|
+
});
|
|
2719
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2720
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2721
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
|
|
2722
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
|
|
2723
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2724
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2725
|
+
await act(async () => {
|
|
2726
|
+
await Promise.resolve();
|
|
2727
|
+
});
|
|
2728
|
+
await waitFor(() => {
|
|
2729
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2730
|
+
});
|
|
2731
|
+
expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/taskforce/auth/session')).length).toBeGreaterThanOrEqual(2);
|
|
2732
|
+
await act(async () => {
|
|
2733
|
+
releaseInitialSession?.();
|
|
2734
|
+
await Promise.resolve();
|
|
2735
|
+
});
|
|
2736
|
+
await waitFor(() => {
|
|
2737
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2738
|
+
});
|
|
2739
|
+
expect(screen.queryByText('Sign in with your account credentials.')).not.toBeInTheDocument();
|
|
2740
|
+
});
|
|
2741
|
+
it('routes successful registration directly into the app when onboarding auto-grants free and the new cloud user already has an accessible workspace', async () => {
|
|
2742
|
+
const user = userEvent.setup();
|
|
2743
|
+
let authenticated = false;
|
|
2744
|
+
const fetchMock = vi.fn((input) => {
|
|
2745
|
+
const url = String(input);
|
|
2746
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2747
|
+
return jsonResponse({
|
|
2748
|
+
config: {
|
|
2749
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2750
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2751
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2752
|
+
runtimeMode: 'cloud',
|
|
2753
|
+
authSource: 'cloud',
|
|
2754
|
+
workspaceMode: 'multi-cloud',
|
|
2755
|
+
workspaceSwitchingEnabled: true,
|
|
2756
|
+
authRequiredForApi: true
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2761
|
+
return jsonResponse({
|
|
2762
|
+
authenticated,
|
|
2763
|
+
runtimeMode: 'cloud',
|
|
2764
|
+
authRequiredForApi: true,
|
|
2765
|
+
userId: authenticated ? 'user-free-2' : null,
|
|
2766
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2767
|
+
betaAccess: true
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2771
|
+
return jsonResponse({
|
|
2772
|
+
...mockConfig,
|
|
2773
|
+
runtimeMode: 'cloud',
|
|
2774
|
+
authRequiredForApi: true,
|
|
2775
|
+
workspaceSwitchingEnabled: true,
|
|
2776
|
+
workspaceId: 'workspace-1',
|
|
2777
|
+
setupState: {
|
|
2778
|
+
globalSetupState: 'present',
|
|
2779
|
+
workspaceSetupState: 'present',
|
|
2780
|
+
runtimeMode: 'cloud',
|
|
2781
|
+
workspaceId: 'workspace-1',
|
|
2782
|
+
mode: 'core',
|
|
2783
|
+
forceSetup: false,
|
|
2784
|
+
forceGlobalSetup: false,
|
|
2785
|
+
forceWorkspaceSetup: false
|
|
2786
|
+
}
|
|
1602
2787
|
});
|
|
1603
2788
|
}
|
|
1604
|
-
if (url.includes('/api/taskforce/
|
|
1605
|
-
return
|
|
2789
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2790
|
+
return jsonResponse({
|
|
2791
|
+
success: true,
|
|
2792
|
+
workspaces: [{
|
|
2793
|
+
id: 'workspace-1',
|
|
2794
|
+
slug: 'workspace-1',
|
|
2795
|
+
name: 'Workspace 1',
|
|
2796
|
+
description: null,
|
|
2797
|
+
role: 'owner',
|
|
2798
|
+
status: 'active',
|
|
2799
|
+
betaAccess: true
|
|
2800
|
+
}],
|
|
2801
|
+
currentWorkspaceId: 'workspace-1'
|
|
2802
|
+
});
|
|
1606
2803
|
}
|
|
1607
|
-
|
|
2804
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2805
|
+
return jsonResponse({
|
|
2806
|
+
gate: 'ok',
|
|
2807
|
+
hasEntitlement: true,
|
|
2808
|
+
canAccessApp: true,
|
|
2809
|
+
canAccessPlans: true,
|
|
2810
|
+
message: null
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2814
|
+
return jsonResponse({ plans: [] });
|
|
2815
|
+
}
|
|
2816
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2817
|
+
authenticated = true;
|
|
2818
|
+
return jsonResponse({
|
|
2819
|
+
success: true,
|
|
2820
|
+
workspaceSetupRequired: false,
|
|
2821
|
+
verificationRequired: false,
|
|
2822
|
+
commercialState: 'active'
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2826
|
+
return jsonResponse(mockCategories);
|
|
2827
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2828
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2829
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2830
|
+
return jsonResponse({ archived: [] });
|
|
2831
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2832
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2833
|
+
if (url.includes('/api/taskforce/types'))
|
|
2834
|
+
return jsonResponse({ types: [] });
|
|
2835
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2836
|
+
return jsonResponse({ priorities: [] });
|
|
2837
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2838
|
+
return jsonResponse({ approaches: [] });
|
|
2839
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2840
|
+
return jsonResponse({ specialists: [] });
|
|
2841
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2842
|
+
return jsonResponse({ templates: [] });
|
|
2843
|
+
return jsonResponse({});
|
|
1608
2844
|
});
|
|
1609
2845
|
vi.stubGlobal('fetch', fetchMock);
|
|
1610
|
-
|
|
2846
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2847
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
|
|
2848
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
|
|
2849
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2850
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2851
|
+
await act(async () => {
|
|
2852
|
+
await Promise.resolve();
|
|
2853
|
+
});
|
|
1611
2854
|
await waitFor(() => {
|
|
1612
|
-
expect(screen.
|
|
1613
|
-
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1614
|
-
expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
|
|
1615
|
-
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
1616
|
-
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
1617
|
-
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1618
|
-
expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
|
|
2855
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
1619
2856
|
});
|
|
1620
|
-
expect(
|
|
2857
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2858
|
+
expect(screen.queryByRole('heading', { name: 'Subscription Plans' })).not.toBeInTheDocument();
|
|
1621
2859
|
});
|
|
1622
|
-
it('
|
|
2860
|
+
it('shows verification before workspace setup when registration still requires email verification', async () => {
|
|
1623
2861
|
const user = userEvent.setup();
|
|
1624
2862
|
const fetchMock = vi.fn((input) => {
|
|
1625
2863
|
const url = String(input);
|
|
@@ -1629,7 +2867,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1629
2867
|
json: () => Promise.resolve({
|
|
1630
2868
|
authenticated: false,
|
|
1631
2869
|
runtimeMode: 'cloud',
|
|
1632
|
-
authRequiredForApi:
|
|
2870
|
+
authRequiredForApi: true
|
|
1633
2871
|
})
|
|
1634
2872
|
});
|
|
1635
2873
|
}
|
|
@@ -1639,12 +2877,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1639
2877
|
json: () => Promise.resolve({
|
|
1640
2878
|
...mockConfig,
|
|
1641
2879
|
runtimeMode: 'cloud',
|
|
1642
|
-
authRequiredForApi:
|
|
2880
|
+
authRequiredForApi: true,
|
|
1643
2881
|
setupState: {
|
|
1644
2882
|
globalSetupState: 'present',
|
|
1645
2883
|
workspaceSetupState: 'present',
|
|
1646
2884
|
runtimeMode: 'cloud',
|
|
1647
|
-
workspaceId: '
|
|
2885
|
+
workspaceId: 'default',
|
|
1648
2886
|
mode: 'core',
|
|
1649
2887
|
forceSetup: false,
|
|
1650
2888
|
forceGlobalSetup: false,
|
|
@@ -1666,8 +2904,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1666
2904
|
ok: true,
|
|
1667
2905
|
json: () => Promise.resolve({
|
|
1668
2906
|
success: true,
|
|
1669
|
-
workspaceSetupRequired:
|
|
1670
|
-
verificationRequired:
|
|
2907
|
+
workspaceSetupRequired: true,
|
|
2908
|
+
verificationRequired: true,
|
|
2909
|
+
emailSent: true
|
|
1671
2910
|
})
|
|
1672
2911
|
});
|
|
1673
2912
|
}
|
|
@@ -1680,10 +2919,10 @@ describe('TaskforceCore Integration', () => {
|
|
|
1680
2919
|
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1681
2920
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1682
2921
|
await waitFor(() => {
|
|
1683
|
-
expect(screen.
|
|
2922
|
+
expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
|
|
1684
2923
|
});
|
|
1685
|
-
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
1686
2924
|
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
2925
|
+
expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
|
|
1687
2926
|
});
|
|
1688
2927
|
it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
|
|
1689
2928
|
const user = userEvent.setup();
|
|
@@ -1820,7 +3059,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1820
3059
|
json: () => Promise.resolve({
|
|
1821
3060
|
success: false,
|
|
1822
3061
|
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
1823
|
-
error: 'Configured
|
|
3062
|
+
error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
|
|
1824
3063
|
})
|
|
1825
3064
|
});
|
|
1826
3065
|
}
|
|
@@ -2072,36 +3311,211 @@ describe('TaskforceCore Integration', () => {
|
|
|
2072
3311
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2073
3312
|
}
|
|
2074
3313
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2075
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3314
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3315
|
+
}
|
|
3316
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3317
|
+
});
|
|
3318
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3319
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
3320
|
+
await waitFor(() => {
|
|
3321
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3322
|
+
});
|
|
3323
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
3324
|
+
await user.clear(nameInput);
|
|
3325
|
+
await user.type(nameInput, 'Delta Project');
|
|
3326
|
+
await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Workspace desc');
|
|
3327
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3328
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
3329
|
+
await waitFor(() => {
|
|
3330
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3331
|
+
});
|
|
3332
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
|
|
3333
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
3334
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
3335
|
+
return false;
|
|
3336
|
+
const init = call[1];
|
|
3337
|
+
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
3338
|
+
return false;
|
|
3339
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
3340
|
+
return body?.stateKey === 'workspace-sync';
|
|
3341
|
+
});
|
|
3342
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
3343
|
+
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
3344
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
|
|
3345
|
+
});
|
|
3346
|
+
it('creates the first cloud workspace through the workspace creation flow during setup', async () => {
|
|
3347
|
+
const user = userEvent.setup();
|
|
3348
|
+
let workspaceCreated = false;
|
|
3349
|
+
const fetchMock = vi.fn((input, init) => {
|
|
3350
|
+
const url = String(input);
|
|
3351
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3352
|
+
return jsonResponse({
|
|
3353
|
+
success: true,
|
|
3354
|
+
config: {
|
|
3355
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3356
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3357
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3358
|
+
runtimeMode: 'cloud',
|
|
3359
|
+
authSource: 'cloud',
|
|
3360
|
+
workspaceMode: 'multi-cloud',
|
|
3361
|
+
workspaceSwitchingEnabled: true,
|
|
3362
|
+
cloudAuthViaLocalProxy: false
|
|
3363
|
+
}
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3367
|
+
return jsonResponse({
|
|
3368
|
+
authenticated: true,
|
|
3369
|
+
runtimeMode: 'cloud',
|
|
3370
|
+
authRequiredForApi: true,
|
|
3371
|
+
userId: 'user-cloud-1',
|
|
3372
|
+
workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3376
|
+
return jsonResponse({
|
|
3377
|
+
gate: 'ok',
|
|
3378
|
+
hasEntitlement: true,
|
|
3379
|
+
canAccessApp: true,
|
|
3380
|
+
canAccessPlans: true,
|
|
3381
|
+
message: null
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3385
|
+
return jsonResponse({
|
|
3386
|
+
...mockConfig,
|
|
3387
|
+
runtimeMode: 'cloud',
|
|
3388
|
+
authRequiredForApi: true,
|
|
3389
|
+
userId: 'user-cloud-1',
|
|
3390
|
+
projectName: workspaceCreated ? 'Cloud Mission' : '',
|
|
3391
|
+
setupState: {
|
|
3392
|
+
globalSetupState: 'present',
|
|
3393
|
+
workspaceSetupState: workspaceCreated ? 'present' : 'missing',
|
|
3394
|
+
runtimeMode: 'cloud',
|
|
3395
|
+
workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default',
|
|
3396
|
+
mode: 'core',
|
|
3397
|
+
forceSetup: false,
|
|
3398
|
+
forceGlobalSetup: false,
|
|
3399
|
+
forceWorkspaceSetup: false,
|
|
3400
|
+
workspace: workspaceCreated
|
|
3401
|
+
? { id: 'workspace-cloud-new', slug: 'workspace-cloud-new', name: 'Cloud Mission', description: 'Cloud workspace desc' }
|
|
3402
|
+
: null,
|
|
3403
|
+
suggestedWorkspaceName: ''
|
|
3404
|
+
},
|
|
3405
|
+
runtimeCapabilities: {
|
|
3406
|
+
runtimeMode: 'cloud',
|
|
3407
|
+
supportsCloudAuth: true,
|
|
3408
|
+
supportsAdminManagedSetupOverrides: true,
|
|
3409
|
+
supportsPackageScriptSetupOverrides: false
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
if (url.includes('/api/taskforce/workspaces') && String(init?.method || 'GET').toUpperCase() === 'GET') {
|
|
3414
|
+
return jsonResponse({
|
|
3415
|
+
success: true,
|
|
3416
|
+
workspaces: workspaceCreated
|
|
3417
|
+
? [{
|
|
3418
|
+
id: 'workspace-cloud-new',
|
|
3419
|
+
slug: 'workspace-cloud-new',
|
|
3420
|
+
name: 'Cloud Mission',
|
|
3421
|
+
description: 'Cloud workspace desc',
|
|
3422
|
+
role: 'owner',
|
|
3423
|
+
status: 'active',
|
|
3424
|
+
betaAccess: true
|
|
3425
|
+
}]
|
|
3426
|
+
: [],
|
|
3427
|
+
currentWorkspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
if (url.includes('/api/taskforce/workspaces') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
3431
|
+
workspaceCreated = true;
|
|
3432
|
+
return jsonResponse({
|
|
3433
|
+
success: true,
|
|
3434
|
+
workspace: {
|
|
3435
|
+
id: 'workspace-cloud-new',
|
|
3436
|
+
slug: 'workspace-cloud-new',
|
|
3437
|
+
name: 'Cloud Mission',
|
|
3438
|
+
description: 'Cloud workspace desc',
|
|
3439
|
+
role: 'owner',
|
|
3440
|
+
status: 'active',
|
|
3441
|
+
betaAccess: true
|
|
3442
|
+
}
|
|
3443
|
+
}, 201);
|
|
3444
|
+
}
|
|
3445
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3446
|
+
return jsonResponse({
|
|
3447
|
+
success: true,
|
|
3448
|
+
workspaceId: 'workspace-cloud-new',
|
|
3449
|
+
role: 'owner'
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3453
|
+
return jsonResponse({ success: true, state: {} });
|
|
3454
|
+
}
|
|
3455
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3456
|
+
return jsonResponse({
|
|
3457
|
+
success: true,
|
|
3458
|
+
state: {
|
|
3459
|
+
version: 2,
|
|
3460
|
+
enabled: false,
|
|
3461
|
+
phase: 'idle',
|
|
3462
|
+
pullCursor: null,
|
|
3463
|
+
lastPullAt: null,
|
|
3464
|
+
lastPushAt: null,
|
|
3465
|
+
lastSyncedAt: null
|
|
3466
|
+
}
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
3470
|
+
return jsonResponse({ success: true });
|
|
3471
|
+
}
|
|
3472
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
3473
|
+
return jsonResponse(mockCategories);
|
|
3474
|
+
}
|
|
3475
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3476
|
+
return jsonResponse({ tasks: mockTasks });
|
|
3477
|
+
}
|
|
3478
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
3479
|
+
return jsonResponse({ archived: [] });
|
|
3480
|
+
}
|
|
3481
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
3482
|
+
return jsonResponse({ dataVersion: 1 });
|
|
3483
|
+
}
|
|
3484
|
+
if (url.includes('/api/taskforce/types')) {
|
|
3485
|
+
return jsonResponse({ types: [] });
|
|
3486
|
+
}
|
|
3487
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
3488
|
+
return jsonResponse({ priorities: [] });
|
|
3489
|
+
}
|
|
3490
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
3491
|
+
return jsonResponse({ approaches: [] });
|
|
3492
|
+
}
|
|
3493
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
3494
|
+
return jsonResponse({ specialists: [] });
|
|
3495
|
+
}
|
|
3496
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3497
|
+
return jsonResponse({ templates: [] });
|
|
2076
3498
|
}
|
|
2077
|
-
return
|
|
3499
|
+
return jsonResponse({});
|
|
2078
3500
|
});
|
|
2079
3501
|
vi.stubGlobal('fetch', fetchMock);
|
|
2080
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
3502
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
|
|
2081
3503
|
await waitFor(() => {
|
|
2082
|
-
expect(screen.getByText('
|
|
3504
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2083
3505
|
});
|
|
2084
|
-
|
|
3506
|
+
expect(screen.getByLabelText('Cloud runtime')).toBeInTheDocument();
|
|
3507
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
2085
3508
|
await user.clear(nameInput);
|
|
2086
|
-
await user.type(nameInput, '
|
|
2087
|
-
await user.type(screen.getByPlaceholderText('
|
|
2088
|
-
await user.click(screen.getByRole('button', { name: '
|
|
3509
|
+
await user.type(nameInput, 'Cloud Mission');
|
|
3510
|
+
await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Cloud workspace desc');
|
|
3511
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3512
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
2089
3513
|
await waitFor(() => {
|
|
2090
3514
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2091
3515
|
});
|
|
2092
|
-
expect(fetchMock).
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
return false;
|
|
2096
|
-
const init = call[1];
|
|
2097
|
-
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
2098
|
-
return false;
|
|
2099
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
2100
|
-
return body?.stateKey === 'workspace-sync';
|
|
2101
|
-
});
|
|
2102
|
-
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
2103
|
-
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
2104
|
-
expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
|
|
3516
|
+
expect(fetchMock.mock.calls.some((call) => (String(call[0]).includes('/api/taskforce/workspaces')
|
|
3517
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'))).toBe(true);
|
|
3518
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspace-profile'))).toBe(false);
|
|
2105
3519
|
});
|
|
2106
3520
|
it('applies the selected starter taxonomy pack during workspace setup', async () => {
|
|
2107
3521
|
const user = userEvent.setup();
|
|
@@ -2209,10 +3623,22 @@ describe('TaskforceCore Integration', () => {
|
|
|
2209
3623
|
vi.stubGlobal('fetch', fetchMock);
|
|
2210
3624
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2211
3625
|
await waitFor(() => {
|
|
2212
|
-
expect(screen.getByText('
|
|
2213
|
-
});
|
|
2214
|
-
|
|
2215
|
-
await user.click(screen.getByRole('button', { name: '
|
|
3626
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3627
|
+
});
|
|
3628
|
+
expect(screen.getByLabelText('Local runtime')).toBeInTheDocument();
|
|
3629
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3630
|
+
expect(screen.queryByRole('radio', { name: /create new local workspace/i })).not.toBeInTheDocument();
|
|
3631
|
+
expect(screen.queryByRole('radio', { name: /sync existing cloud workspace/i })).not.toBeInTheDocument();
|
|
3632
|
+
expect(screen.queryByText(/Includes \d+ categories?/i)).not.toBeInTheDocument();
|
|
3633
|
+
expect(screen.getByRole('option', { name: 'General' })).toBeInTheDocument();
|
|
3634
|
+
expect(screen.getByText('Task')).toBeInTheDocument();
|
|
3635
|
+
expect(screen.getByText('Low')).toBeInTheDocument();
|
|
3636
|
+
await user.selectOptions(screen.getByRole('combobox'), 'software-development');
|
|
3637
|
+
expect(screen.getByText('Engineering')).toBeInTheDocument();
|
|
3638
|
+
expect(screen.getByText('Feature, Bug')).toBeInTheDocument();
|
|
3639
|
+
expect(screen.getByText('Low, Medium, High, Critical')).toBeInTheDocument();
|
|
3640
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
3641
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
2216
3642
|
await waitFor(() => {
|
|
2217
3643
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2218
3644
|
});
|
|
@@ -2305,9 +3731,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
2305
3731
|
expect(configCalls).toBeGreaterThan(1);
|
|
2306
3732
|
});
|
|
2307
3733
|
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
2308
|
-
expect(screen.queryByText('
|
|
3734
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2309
3735
|
});
|
|
2310
|
-
it('
|
|
3736
|
+
it('keeps setup blocking while the selected cloud workspace is performing its first local import', async () => {
|
|
2311
3737
|
const user = userEvent.setup();
|
|
2312
3738
|
let workspaceSaved = false;
|
|
2313
3739
|
const fetchMock = vi.fn((url, init) => {
|
|
@@ -2383,9 +3809,45 @@ describe('TaskforceCore Integration', () => {
|
|
|
2383
3809
|
if (url.includes('/api/taskforce/session/workspace')) {
|
|
2384
3810
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2385
3811
|
}
|
|
3812
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3813
|
+
return Promise.resolve({
|
|
3814
|
+
ok: true,
|
|
3815
|
+
json: () => Promise.resolve({
|
|
3816
|
+
success: true,
|
|
3817
|
+
state: workspaceSaved
|
|
3818
|
+
? {
|
|
3819
|
+
version: 2,
|
|
3820
|
+
enabled: true,
|
|
3821
|
+
phase: 'attach-cloud',
|
|
3822
|
+
pullCursor: null,
|
|
3823
|
+
lastPullAt: null,
|
|
3824
|
+
lastPushAt: null,
|
|
3825
|
+
lastSyncedAt: null
|
|
3826
|
+
}
|
|
3827
|
+
: {
|
|
3828
|
+
version: 2,
|
|
3829
|
+
enabled: false,
|
|
3830
|
+
phase: 'idle',
|
|
3831
|
+
pullCursor: null,
|
|
3832
|
+
lastPullAt: null,
|
|
3833
|
+
lastPushAt: null,
|
|
3834
|
+
lastSyncedAt: null
|
|
3835
|
+
}
|
|
3836
|
+
})
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
2386
3839
|
if (url.includes('/api/taskforce/ui-state')) {
|
|
2387
3840
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2388
3841
|
}
|
|
3842
|
+
if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
|
|
3843
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3844
|
+
}
|
|
3845
|
+
if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
|
|
3846
|
+
return new Promise(() => { });
|
|
3847
|
+
}
|
|
3848
|
+
if (url === '/api/taskforce/sync/workspace/apply-local') {
|
|
3849
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3850
|
+
}
|
|
2389
3851
|
if (url.includes('/api/taskforce/tasks')) {
|
|
2390
3852
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2391
3853
|
}
|
|
@@ -2397,17 +3859,18 @@ describe('TaskforceCore Integration', () => {
|
|
|
2397
3859
|
vi.stubGlobal('fetch', fetchMock);
|
|
2398
3860
|
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2399
3861
|
await waitFor(() => {
|
|
2400
|
-
expect(screen.getByText('
|
|
3862
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2401
3863
|
});
|
|
2402
|
-
await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
|
|
2403
3864
|
await waitFor(() => {
|
|
2404
3865
|
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
2405
3866
|
});
|
|
2406
3867
|
await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
|
|
2407
|
-
await user.click(screen.getByRole('button', { name: '
|
|
3868
|
+
await user.click(screen.getByRole('button', { name: 'Connect Workspace' }));
|
|
2408
3869
|
await waitFor(() => {
|
|
2409
|
-
expect(screen.
|
|
3870
|
+
expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
|
|
2410
3871
|
});
|
|
3872
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3873
|
+
expect(screen.getByRole('button', { name: 'Importing Workspace...' })).toBeInTheDocument();
|
|
2411
3874
|
const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
|
|
2412
3875
|
const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
|
|
2413
3876
|
expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
|
|
@@ -2422,8 +3885,258 @@ describe('TaskforceCore Integration', () => {
|
|
|
2422
3885
|
});
|
|
2423
3886
|
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
2424
3887
|
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
2425
|
-
expect(workspaceSyncStateBody.patch?.
|
|
3888
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
|
|
3889
|
+
expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
|
|
2426
3890
|
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
|
|
3891
|
+
expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
|
|
3892
|
+
});
|
|
3893
|
+
it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
|
|
3894
|
+
const fetchMock = vi.fn((url) => {
|
|
3895
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3896
|
+
return Promise.resolve({
|
|
3897
|
+
ok: true,
|
|
3898
|
+
json: () => Promise.resolve({
|
|
3899
|
+
authenticated: false,
|
|
3900
|
+
runtimeMode: 'local',
|
|
3901
|
+
authRequiredForApi: false
|
|
3902
|
+
})
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3906
|
+
return Promise.resolve({
|
|
3907
|
+
ok: true,
|
|
3908
|
+
json: () => Promise.resolve({
|
|
3909
|
+
...mockConfig,
|
|
3910
|
+
runtimeMode: 'local',
|
|
3911
|
+
authRequiredForApi: false,
|
|
3912
|
+
setupState: {
|
|
3913
|
+
globalSetupState: 'present',
|
|
3914
|
+
workspaceSetupState: 'missing',
|
|
3915
|
+
runtimeMode: 'local',
|
|
3916
|
+
workspaceId: 'default',
|
|
3917
|
+
mode: 'core',
|
|
3918
|
+
forceSetup: false,
|
|
3919
|
+
forceGlobalSetup: false,
|
|
3920
|
+
forceWorkspaceSetup: true,
|
|
3921
|
+
workspace: null,
|
|
3922
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
3923
|
+
},
|
|
3924
|
+
runtimeCapabilities: {
|
|
3925
|
+
runtimeMode: 'local',
|
|
3926
|
+
supportsCloudAuth: true,
|
|
3927
|
+
supportsAdminManagedSetupOverrides: false,
|
|
3928
|
+
supportsPackageScriptSetupOverrides: true
|
|
3929
|
+
}
|
|
3930
|
+
})
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3934
|
+
});
|
|
3935
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3936
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3937
|
+
await waitFor(() => {
|
|
3938
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
3939
|
+
});
|
|
3940
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
3941
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
3942
|
+
expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeDisabled();
|
|
3943
|
+
});
|
|
3944
|
+
it('returns signed-in users to the cloud workspace picker after setup sign-in', async () => {
|
|
3945
|
+
const user = userEvent.setup();
|
|
3946
|
+
let authenticated = false;
|
|
3947
|
+
const fetchMock = vi.fn((url, init) => {
|
|
3948
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3949
|
+
return Promise.resolve({
|
|
3950
|
+
ok: true,
|
|
3951
|
+
json: () => Promise.resolve({
|
|
3952
|
+
authenticated,
|
|
3953
|
+
runtimeMode: 'local',
|
|
3954
|
+
authRequiredForApi: false
|
|
3955
|
+
})
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
3958
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3959
|
+
return Promise.resolve({
|
|
3960
|
+
ok: true,
|
|
3961
|
+
json: () => Promise.resolve({
|
|
3962
|
+
...mockConfig,
|
|
3963
|
+
runtimeMode: 'local',
|
|
3964
|
+
authRequiredForApi: false,
|
|
3965
|
+
setupState: {
|
|
3966
|
+
globalSetupState: 'present',
|
|
3967
|
+
workspaceSetupState: 'missing',
|
|
3968
|
+
runtimeMode: 'local',
|
|
3969
|
+
workspaceId: 'default',
|
|
3970
|
+
mode: 'core',
|
|
3971
|
+
forceSetup: false,
|
|
3972
|
+
forceGlobalSetup: false,
|
|
3973
|
+
forceWorkspaceSetup: true,
|
|
3974
|
+
workspace: null,
|
|
3975
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
3976
|
+
},
|
|
3977
|
+
runtimeCapabilities: {
|
|
3978
|
+
runtimeMode: 'local',
|
|
3979
|
+
supportsCloudAuth: true,
|
|
3980
|
+
supportsAdminManagedSetupOverrides: false,
|
|
3981
|
+
supportsPackageScriptSetupOverrides: true
|
|
3982
|
+
}
|
|
3983
|
+
})
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
if (url.includes('/api/taskforce/auth/login') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
3987
|
+
authenticated = true;
|
|
3988
|
+
return Promise.resolve({
|
|
3989
|
+
ok: true,
|
|
3990
|
+
json: () => Promise.resolve({
|
|
3991
|
+
success: true,
|
|
3992
|
+
workspaceSetupRequired: true
|
|
3993
|
+
})
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
3997
|
+
return Promise.resolve({
|
|
3998
|
+
ok: true,
|
|
3999
|
+
json: () => Promise.resolve({
|
|
4000
|
+
success: true,
|
|
4001
|
+
workspaces: [
|
|
4002
|
+
{
|
|
4003
|
+
id: 'workspace-cloud-123',
|
|
4004
|
+
name: 'Cloud Project 123',
|
|
4005
|
+
description: 'Cloud desc'
|
|
4006
|
+
}
|
|
4007
|
+
]
|
|
4008
|
+
})
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4012
|
+
});
|
|
4013
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4014
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4015
|
+
await waitFor(() => {
|
|
4016
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
4017
|
+
});
|
|
4018
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
4019
|
+
await waitFor(() => {
|
|
4020
|
+
expect(screen.getByTestId('auth-email-input')).toBeInTheDocument();
|
|
4021
|
+
});
|
|
4022
|
+
await user.type(screen.getByTestId('auth-email-input'), 'agent@example.com');
|
|
4023
|
+
await user.type(screen.getByTestId('auth-password-input'), 'password123');
|
|
4024
|
+
await user.click(screen.getByTestId('auth-submit-button'));
|
|
4025
|
+
await waitFor(() => {
|
|
4026
|
+
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
4027
|
+
});
|
|
4028
|
+
await waitFor(() => {
|
|
4029
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
4030
|
+
expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeEnabled();
|
|
4031
|
+
});
|
|
4032
|
+
});
|
|
4033
|
+
it('loads workspace setup after cloud sign-in when the user has no workspaces', async () => {
|
|
4034
|
+
const user = userEvent.setup();
|
|
4035
|
+
let authenticated = false;
|
|
4036
|
+
const fetchMock = vi.fn((input, init) => {
|
|
4037
|
+
const url = String(input);
|
|
4038
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4039
|
+
return jsonResponse({
|
|
4040
|
+
success: true,
|
|
4041
|
+
config: {
|
|
4042
|
+
baseUrl: 'https://app.taskforce.example',
|
|
4043
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
4044
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
4045
|
+
runtimeMode: 'cloud',
|
|
4046
|
+
authSource: 'cloud',
|
|
4047
|
+
workspaceMode: 'multi-cloud',
|
|
4048
|
+
workspaceSwitchingEnabled: true,
|
|
4049
|
+
cloudAuthViaLocalProxy: false
|
|
4050
|
+
}
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4054
|
+
return jsonResponse({
|
|
4055
|
+
authenticated,
|
|
4056
|
+
runtimeMode: 'cloud',
|
|
4057
|
+
authRequiredForApi: true,
|
|
4058
|
+
userId: authenticated ? 'user-cloud-setup' : 'anonymous',
|
|
4059
|
+
workspaceId: 'default'
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
4062
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
4063
|
+
authenticated = true;
|
|
4064
|
+
return jsonResponse({ success: true });
|
|
4065
|
+
}
|
|
4066
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
4067
|
+
return jsonResponse({
|
|
4068
|
+
success: true,
|
|
4069
|
+
workspaces: [],
|
|
4070
|
+
currentWorkspaceId: 'default'
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
4074
|
+
return jsonResponse({
|
|
4075
|
+
gate: 'ok',
|
|
4076
|
+
hasEntitlement: true,
|
|
4077
|
+
canAccessApp: true,
|
|
4078
|
+
canAccessPlans: true,
|
|
4079
|
+
message: null
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4083
|
+
return jsonResponse({
|
|
4084
|
+
...mockConfig,
|
|
4085
|
+
runtimeMode: 'cloud',
|
|
4086
|
+
authRequiredForApi: true,
|
|
4087
|
+
userId: authenticated ? 'user-cloud-setup' : 'anonymous',
|
|
4088
|
+
setupState: {
|
|
4089
|
+
globalSetupState: 'present',
|
|
4090
|
+
workspaceSetupState: authenticated ? 'missing' : 'present',
|
|
4091
|
+
runtimeMode: 'cloud',
|
|
4092
|
+
workspaceId: 'default',
|
|
4093
|
+
mode: 'core',
|
|
4094
|
+
forceSetup: false,
|
|
4095
|
+
forceGlobalSetup: false,
|
|
4096
|
+
forceWorkspaceSetup: false,
|
|
4097
|
+
workspace: null,
|
|
4098
|
+
suggestedWorkspaceName: ''
|
|
4099
|
+
},
|
|
4100
|
+
runtimeCapabilities: {
|
|
4101
|
+
runtimeMode: 'cloud',
|
|
4102
|
+
supportsCloudAuth: true,
|
|
4103
|
+
supportsAdminManagedSetupOverrides: true,
|
|
4104
|
+
supportsPackageScriptSetupOverrides: false
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4109
|
+
return jsonResponse({ success: true, state: {} });
|
|
4110
|
+
}
|
|
4111
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4112
|
+
return jsonResponse({
|
|
4113
|
+
success: true,
|
|
4114
|
+
state: {
|
|
4115
|
+
version: 2,
|
|
4116
|
+
enabled: false,
|
|
4117
|
+
phase: 'idle',
|
|
4118
|
+
pullCursor: null,
|
|
4119
|
+
lastPullAt: null,
|
|
4120
|
+
lastPushAt: null,
|
|
4121
|
+
lastSyncedAt: null
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
4126
|
+
return jsonResponse({ templates: [] });
|
|
4127
|
+
}
|
|
4128
|
+
return jsonResponse({});
|
|
4129
|
+
});
|
|
4130
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4131
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=login&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
4132
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'setup@example.com');
|
|
4133
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
4134
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
4135
|
+
await waitFor(() => {
|
|
4136
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4137
|
+
});
|
|
4138
|
+
expect(screen.queryByText('Resolving workspace access...')).not.toBeInTheDocument();
|
|
4139
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/config'))).toBe(true);
|
|
2427
4140
|
});
|
|
2428
4141
|
it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
|
|
2429
4142
|
const user = userEvent.setup();
|
|
@@ -2495,7 +4208,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2495
4208
|
vi.stubGlobal('fetch', fetchMock);
|
|
2496
4209
|
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2497
4210
|
await waitFor(() => {
|
|
2498
|
-
expect(screen.getByText('
|
|
4211
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2499
4212
|
});
|
|
2500
4213
|
await waitFor(() => {
|
|
2501
4214
|
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
@@ -2933,7 +4646,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2933
4646
|
vi.stubGlobal('fetch', fetchMock);
|
|
2934
4647
|
const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2935
4648
|
await waitFor(() => {
|
|
2936
|
-
expect(screen.getByText('
|
|
4649
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
2937
4650
|
});
|
|
2938
4651
|
await waitFor(() => {
|
|
2939
4652
|
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
@@ -2949,7 +4662,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
2949
4662
|
initialRender.unmount();
|
|
2950
4663
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2951
4664
|
await waitFor(() => {
|
|
2952
|
-
expect(screen.queryByText('
|
|
4665
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2953
4666
|
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
2954
4667
|
});
|
|
2955
4668
|
expect(cloudWorkspaceCreated).toBe(false);
|
|
@@ -2961,6 +4674,13 @@ describe('TaskforceCore Integration', () => {
|
|
|
2961
4674
|
expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
|
|
2962
4675
|
expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
|
|
2963
4676
|
});
|
|
4677
|
+
it('keeps the account badge neutral until the authenticated identity is fully resolved', () => {
|
|
4678
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
4679
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
4680
|
+
expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;");
|
|
4681
|
+
expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountAvatar = isAuthenticated && authSessionResolved && accountAvatarUrl.length > 0;");
|
|
4682
|
+
expect(standaloneLayoutSource).toContain('<User size={14} className={accountStyles.avatarIcon} />');
|
|
4683
|
+
});
|
|
2964
4684
|
it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
|
|
2965
4685
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
2966
4686
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
@@ -2968,17 +4688,25 @@ describe('TaskforceCore Integration', () => {
|
|
|
2968
4688
|
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
2969
4689
|
expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
|
|
2970
4690
|
expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
|
|
2971
|
-
expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
|
|
2972
|
-
expect(standaloneLayoutSource).toContain("
|
|
4691
|
+
expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(accountProfileSummary?.workspaceId || authWorkspaceId || currentWorkspaceId || '').trim();");
|
|
4692
|
+
expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
2973
4693
|
expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
|
|
2974
4694
|
expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
|
|
2975
4695
|
});
|
|
2976
4696
|
it('routes local plans and billing UI through the connected cloud billing base', () => {
|
|
2977
4697
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
2978
4698
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
4699
|
+
const plansPagePath = path.join(process.cwd(), 'src/components/views/PlansPage.tsx');
|
|
4700
|
+
const plansPageSource = fs.readFileSync(plansPagePath, 'utf8');
|
|
4701
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
4702
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
4703
|
+
expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
4704
|
+
expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl('/api/taskforce/billing/portal-session')");
|
|
4705
|
+
expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl={resolveCloudAuthUrl}");
|
|
4706
|
+
expect(plansPageSource).toContain('const buildCloudBillingUrl = useCallback((path: string): string => {');
|
|
4707
|
+
expect(plansPageSource).toContain("const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
4708
|
+
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
|
|
4709
|
+
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
|
|
2982
4710
|
});
|
|
2983
4711
|
it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
|
|
2984
4712
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
@@ -3175,18 +4903,31 @@ describe('TaskforceCore Integration', () => {
|
|
|
3175
4903
|
vi.stubGlobal('fetch', fetchMock);
|
|
3176
4904
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3177
4905
|
await waitFor(() => {
|
|
3178
|
-
expect(screen.getByTitle(
|
|
4906
|
+
expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
|
|
3179
4907
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3180
4908
|
});
|
|
3181
4909
|
await user.click(screen.getByTitle(/Account/i));
|
|
3182
4910
|
await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
|
|
3183
4911
|
await waitFor(() => {
|
|
3184
4912
|
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
|
|
3185
|
-
expect(screen.getByTitle(
|
|
4913
|
+
expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
|
|
3186
4914
|
});
|
|
3187
4915
|
await user.click(screen.getByTitle(/Account/i));
|
|
3188
4916
|
expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
|
|
3189
4917
|
});
|
|
4918
|
+
it('uses an explicit cloud sign-out redirect and broader cloud logout reset path', () => {
|
|
4919
|
+
const standaloneLayoutSource = fs.readFileSync(path.join(__dirname, 'components/views/StandaloneLayout.tsx'), 'utf8');
|
|
4920
|
+
const useTaskforceSource = fs.readFileSync(path.join(__dirname, 'hooks/useTaskforce.ts'), 'utf8');
|
|
4921
|
+
expect(standaloneLayoutSource).toContain("await logout();");
|
|
4922
|
+
expect(standaloneLayoutSource).toContain("if (runtimeMode === 'cloud') {");
|
|
4923
|
+
expect(standaloneLayoutSource).toContain("navigate('/login', { replace: true });");
|
|
4924
|
+
expect(useTaskforceSource).toContain('const resetCloudLogoutClientState = useCallback(() => {');
|
|
4925
|
+
expect(useTaskforceSource).toContain('setArchivedTasks([]);');
|
|
4926
|
+
expect(useTaskforceSource).toContain('setDeletedTasks([]);');
|
|
4927
|
+
expect(useTaskforceSource).toContain('setAssigneeOptions(createDefaultAssigneeOptions());');
|
|
4928
|
+
expect(useTaskforceSource).toContain('setUiStateReady(false);');
|
|
4929
|
+
expect(useTaskforceSource).toContain('resetCloudLogoutClientState();');
|
|
4930
|
+
});
|
|
3190
4931
|
it('does not pull from cloud when workspace sync toggle is disabled', async () => {
|
|
3191
4932
|
const fetchMock = vi.fn((url) => {
|
|
3192
4933
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -4055,9 +5796,10 @@ describe('TaskforceCore Integration', () => {
|
|
|
4055
5796
|
it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
|
|
4056
5797
|
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
4057
5798
|
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
4058
|
-
expect(useTaskforceSource).toContain("const
|
|
4059
|
-
expect(useTaskforceSource).toContain('Local runtime
|
|
4060
|
-
expect(useTaskforceSource).toContain('
|
|
5799
|
+
expect(useTaskforceSource).toContain("const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;");
|
|
5800
|
+
expect(useTaskforceSource).toContain('Local runtime workspace identity must be sourced from local DB (/api/taskforce/config),');
|
|
5801
|
+
expect(useTaskforceSource).toContain('not from persisted browser context or cloud session payloads.');
|
|
5802
|
+
expect(useTaskforceSource).toContain("if (initialRuntimeMode === 'local') return 'default';");
|
|
4061
5803
|
});
|
|
4062
5804
|
it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
|
|
4063
5805
|
const fetchMock = vi.fn((url, init) => {
|