@taskforcehq/taskforce 0.3.301 → 0.3.303
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 +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- 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/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- 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 +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -48,9 +48,10 @@ 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
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
54
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
54
55
|
}
|
|
55
56
|
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
56
57
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
@@ -168,7 +169,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
168
169
|
it('shows the header sync badge as off when local runtime is signed out', async () => {
|
|
169
170
|
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
170
171
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
171
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
172
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
172
173
|
}
|
|
173
174
|
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
174
175
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
@@ -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,7 +1570,293 @@ describe('TaskforceCore Integration', () => {
|
|
|
1569
1570
|
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1570
1571
|
});
|
|
1571
1572
|
}, 10000);
|
|
1572
|
-
it('
|
|
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
|
+
});
|
|
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')) {
|
|
1575
1862
|
return Promise.resolve({
|
|
@@ -1619,14 +1906,16 @@ describe('TaskforceCore Integration', () => {
|
|
|
1619
1906
|
});
|
|
1620
1907
|
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
1621
1908
|
});
|
|
1622
|
-
it('
|
|
1623
|
-
const
|
|
1909
|
+
it('does not switch to verify mode after registration when verification is not required', async () => {
|
|
1910
|
+
const user = userEvent.setup();
|
|
1911
|
+
const fetchMock = vi.fn((input) => {
|
|
1912
|
+
const url = String(input);
|
|
1624
1913
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
1625
1914
|
return Promise.resolve({
|
|
1626
1915
|
ok: true,
|
|
1627
1916
|
json: () => Promise.resolve({
|
|
1628
1917
|
authenticated: false,
|
|
1629
|
-
runtimeMode: '
|
|
1918
|
+
runtimeMode: 'cloud',
|
|
1630
1919
|
authRequiredForApi: false
|
|
1631
1920
|
})
|
|
1632
1921
|
});
|
|
@@ -1636,12 +1925,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1636
1925
|
ok: true,
|
|
1637
1926
|
json: () => Promise.resolve({
|
|
1638
1927
|
...mockConfig,
|
|
1639
|
-
runtimeMode: '
|
|
1928
|
+
runtimeMode: 'cloud',
|
|
1640
1929
|
authRequiredForApi: false,
|
|
1641
1930
|
setupState: {
|
|
1642
1931
|
globalSetupState: 'present',
|
|
1643
1932
|
workspaceSetupState: 'present',
|
|
1644
|
-
runtimeMode: '
|
|
1933
|
+
runtimeMode: 'cloud',
|
|
1645
1934
|
workspaceId: 'workspace-local-active',
|
|
1646
1935
|
mode: 'core',
|
|
1647
1936
|
forceSetup: false,
|
|
@@ -1651,99 +1940,373 @@ describe('TaskforceCore Integration', () => {
|
|
|
1651
1940
|
})
|
|
1652
1941
|
});
|
|
1653
1942
|
}
|
|
1654
|
-
if (url.includes('/api/taskforce/
|
|
1655
|
-
return Promise.resolve({
|
|
1943
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1944
|
+
return Promise.resolve({
|
|
1945
|
+
ok: true,
|
|
1946
|
+
json: () => Promise.resolve({
|
|
1947
|
+
plans: []
|
|
1948
|
+
})
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1952
|
+
return Promise.resolve({
|
|
1953
|
+
ok: true,
|
|
1954
|
+
json: () => Promise.resolve({
|
|
1955
|
+
success: true,
|
|
1956
|
+
workspaceSetupRequired: false,
|
|
1957
|
+
verificationRequired: false
|
|
1958
|
+
})
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1962
|
+
});
|
|
1963
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1964
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
1965
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
1966
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
1967
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1968
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1969
|
+
await waitFor(() => {
|
|
1970
|
+
expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
|
|
1971
|
+
});
|
|
1972
|
+
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
1973
|
+
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
1974
|
+
});
|
|
1975
|
+
it('routes successful registration into plan selection when onboarding leaves the account pending plan selection', async () => {
|
|
1976
|
+
const user = userEvent.setup();
|
|
1977
|
+
let authenticated = false;
|
|
1978
|
+
const fetchMock = vi.fn((input) => {
|
|
1979
|
+
const url = String(input);
|
|
1980
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1981
|
+
return jsonResponse({
|
|
1982
|
+
config: {
|
|
1983
|
+
baseUrl: 'https://app.taskforce.example',
|
|
1984
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
1985
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
1986
|
+
runtimeMode: 'cloud',
|
|
1987
|
+
authSource: 'cloud',
|
|
1988
|
+
workspaceMode: 'multi-cloud',
|
|
1989
|
+
workspaceSwitchingEnabled: true,
|
|
1990
|
+
authRequiredForApi: true
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1995
|
+
return jsonResponse({
|
|
1996
|
+
authenticated,
|
|
1997
|
+
runtimeMode: 'cloud',
|
|
1998
|
+
authRequiredForApi: true,
|
|
1999
|
+
userId: authenticated ? 'user-1' : null,
|
|
2000
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2001
|
+
betaAccess: true
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2005
|
+
return jsonResponse({
|
|
2006
|
+
...mockConfig,
|
|
2007
|
+
runtimeMode: 'cloud',
|
|
2008
|
+
authRequiredForApi: true,
|
|
2009
|
+
workspaceSwitchingEnabled: true,
|
|
2010
|
+
setupState: {
|
|
2011
|
+
globalSetupState: 'present',
|
|
2012
|
+
workspaceSetupState: 'present',
|
|
2013
|
+
runtimeMode: 'cloud',
|
|
2014
|
+
workspaceId: 'workspace-1',
|
|
2015
|
+
mode: 'core',
|
|
2016
|
+
forceSetup: false,
|
|
2017
|
+
forceGlobalSetup: false,
|
|
2018
|
+
forceWorkspaceSetup: false
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2023
|
+
return jsonResponse({
|
|
2024
|
+
success: true,
|
|
2025
|
+
workspaces: [{
|
|
2026
|
+
id: 'workspace-1',
|
|
2027
|
+
slug: 'workspace-1',
|
|
2028
|
+
name: 'Workspace 1',
|
|
2029
|
+
description: null,
|
|
2030
|
+
role: 'owner',
|
|
2031
|
+
status: 'active',
|
|
2032
|
+
betaAccess: true
|
|
2033
|
+
}],
|
|
2034
|
+
currentWorkspaceId: 'workspace-1'
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2038
|
+
return jsonResponse({
|
|
2039
|
+
gate: 'plan_selection_required',
|
|
2040
|
+
hasEntitlement: true,
|
|
2041
|
+
canAccessApp: false,
|
|
2042
|
+
canAccessPlans: true,
|
|
2043
|
+
message: 'Choose a plan to continue.'
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2047
|
+
return jsonResponse({
|
|
2048
|
+
entitlementState: 'pending_plan_selection',
|
|
2049
|
+
gate: 'plan_selection_required',
|
|
2050
|
+
message: 'Choose a plan to continue.',
|
|
2051
|
+
workspaceId: 'workspace-1'
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2055
|
+
return jsonResponse({
|
|
2056
|
+
plans: [{
|
|
2057
|
+
planId: 'starter',
|
|
2058
|
+
displayName: 'Starter',
|
|
2059
|
+
status: 'active',
|
|
2060
|
+
defaultVersion: {
|
|
2061
|
+
planVersionId: 'starter-v1',
|
|
2062
|
+
versionNumber: 1,
|
|
2063
|
+
seatLimit: null,
|
|
2064
|
+
metadata: {}
|
|
2065
|
+
},
|
|
2066
|
+
features: [],
|
|
2067
|
+
pricing: {
|
|
2068
|
+
month: {
|
|
2069
|
+
planVersionId: 'starter-v1',
|
|
2070
|
+
interval: 'month',
|
|
2071
|
+
pricingType: 'free',
|
|
2072
|
+
stripePriceId: 'free:starter-v1:month',
|
|
2073
|
+
displayAmount: '$0.00',
|
|
2074
|
+
unitAmount: 0,
|
|
2075
|
+
currency: 'usd'
|
|
2076
|
+
},
|
|
2077
|
+
year: null
|
|
2078
|
+
}
|
|
2079
|
+
}]
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2083
|
+
authenticated = true;
|
|
2084
|
+
return jsonResponse({
|
|
2085
|
+
success: true,
|
|
2086
|
+
workspaceSetupRequired: false,
|
|
2087
|
+
verificationRequired: false,
|
|
2088
|
+
planSelectionRequired: true,
|
|
2089
|
+
commercialState: 'pending_plan_selection'
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
2093
|
+
return jsonResponse(mockCategories);
|
|
2094
|
+
}
|
|
2095
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2096
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2097
|
+
}
|
|
2098
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
2099
|
+
return jsonResponse({ archived: [] });
|
|
2100
|
+
}
|
|
2101
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
2102
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2103
|
+
}
|
|
2104
|
+
if (url.includes('/api/taskforce/types')) {
|
|
2105
|
+
return jsonResponse({ types: [] });
|
|
2106
|
+
}
|
|
2107
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
2108
|
+
return jsonResponse({ priorities: [] });
|
|
2109
|
+
}
|
|
2110
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
2111
|
+
return jsonResponse({ approaches: [] });
|
|
2112
|
+
}
|
|
2113
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
2114
|
+
return jsonResponse({ specialists: [] });
|
|
2115
|
+
}
|
|
2116
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2117
|
+
return jsonResponse({ templates: [] });
|
|
2118
|
+
}
|
|
2119
|
+
return jsonResponse({});
|
|
2120
|
+
});
|
|
2121
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2122
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2123
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
|
|
2124
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
|
|
2125
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
2126
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2127
|
+
await waitFor(() => {
|
|
2128
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2129
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
2130
|
+
});
|
|
2131
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
|
|
2132
|
+
});
|
|
2133
|
+
it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
|
|
2134
|
+
const user = userEvent.setup();
|
|
2135
|
+
let authenticated = false;
|
|
2136
|
+
const fetchMock = vi.fn((input) => {
|
|
2137
|
+
const url = String(input);
|
|
2138
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2139
|
+
return jsonResponse({
|
|
2140
|
+
config: {
|
|
2141
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2142
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2143
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2144
|
+
runtimeMode: 'cloud',
|
|
2145
|
+
authSource: 'cloud',
|
|
2146
|
+
workspaceMode: 'multi-cloud',
|
|
2147
|
+
workspaceSwitchingEnabled: true,
|
|
2148
|
+
authRequiredForApi: true
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2153
|
+
return jsonResponse({
|
|
2154
|
+
authenticated,
|
|
2155
|
+
runtimeMode: 'cloud',
|
|
2156
|
+
authRequiredForApi: true,
|
|
2157
|
+
userId: authenticated ? 'user-1' : null,
|
|
2158
|
+
workspaceId: authenticated ? 'workspace-1' : null,
|
|
2159
|
+
betaAccess: true
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2163
|
+
return jsonResponse({
|
|
2164
|
+
...mockConfig,
|
|
2165
|
+
runtimeMode: 'cloud',
|
|
2166
|
+
authRequiredForApi: true,
|
|
2167
|
+
workspaceSwitchingEnabled: true,
|
|
2168
|
+
setupState: {
|
|
2169
|
+
globalSetupState: 'present',
|
|
2170
|
+
workspaceSetupState: 'present',
|
|
2171
|
+
runtimeMode: 'cloud',
|
|
2172
|
+
workspaceId: 'workspace-1',
|
|
2173
|
+
mode: 'core',
|
|
2174
|
+
forceSetup: false,
|
|
2175
|
+
forceGlobalSetup: false,
|
|
2176
|
+
forceWorkspaceSetup: false
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2181
|
+
return jsonResponse({
|
|
2182
|
+
success: true,
|
|
2183
|
+
workspaces: [{
|
|
2184
|
+
id: 'workspace-1',
|
|
2185
|
+
slug: 'workspace-1',
|
|
2186
|
+
name: 'Workspace 1',
|
|
2187
|
+
description: null,
|
|
2188
|
+
role: 'owner',
|
|
2189
|
+
status: 'active',
|
|
2190
|
+
betaAccess: true
|
|
2191
|
+
}],
|
|
2192
|
+
currentWorkspaceId: 'workspace-1'
|
|
2193
|
+
});
|
|
1656
2194
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
expect(screen.queryByRole('button', { name: 'Fill Form' })).not.toBeInTheDocument();
|
|
1665
|
-
firstRender.unmount();
|
|
1666
|
-
globalThis.__DEBUG_MODE__ = true;
|
|
1667
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1668
|
-
expect(await screen.findByRole('button', { name: 'Fill Form' })).toBeInTheDocument();
|
|
1669
|
-
});
|
|
1670
|
-
it('fills and submits generated debug registration credentials through the normal register flow', async () => {
|
|
1671
|
-
globalThis.__DEBUG_MODE__ = true;
|
|
1672
|
-
const user = userEvent.setup();
|
|
1673
|
-
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1735689600000);
|
|
1674
|
-
const fetchMock = vi.fn((input, init) => {
|
|
1675
|
-
const url = String(input);
|
|
1676
|
-
if (url.includes('/api/taskforce/auth/session')) {
|
|
1677
|
-
return Promise.resolve({
|
|
1678
|
-
ok: true,
|
|
1679
|
-
json: () => Promise.resolve({
|
|
1680
|
-
authenticated: false,
|
|
1681
|
-
runtimeMode: 'local',
|
|
1682
|
-
authRequiredForApi: false
|
|
1683
|
-
})
|
|
2195
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2196
|
+
return jsonResponse({
|
|
2197
|
+
gate: 'checkout_pending',
|
|
2198
|
+
hasEntitlement: true,
|
|
2199
|
+
canAccessApp: false,
|
|
2200
|
+
canAccessPlans: true,
|
|
2201
|
+
message: 'Complete checkout to continue.'
|
|
1684
2202
|
});
|
|
1685
2203
|
}
|
|
1686
|
-
if (url.includes('/api/taskforce/
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
workspaceSetupState: 'present',
|
|
1696
|
-
runtimeMode: 'local',
|
|
1697
|
-
workspaceId: 'workspace-local-active',
|
|
1698
|
-
mode: 'core',
|
|
1699
|
-
forceSetup: false,
|
|
1700
|
-
forceGlobalSetup: false,
|
|
1701
|
-
forceWorkspaceSetup: false
|
|
1702
|
-
}
|
|
1703
|
-
})
|
|
2204
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2205
|
+
return jsonResponse({
|
|
2206
|
+
planId: 'pro',
|
|
2207
|
+
planName: 'Pro',
|
|
2208
|
+
planVersionId: 'pro-v1',
|
|
2209
|
+
entitlementState: 'checkout_pending',
|
|
2210
|
+
gate: 'checkout_pending',
|
|
2211
|
+
message: 'Complete checkout to continue.',
|
|
2212
|
+
workspaceId: 'workspace-1'
|
|
1704
2213
|
});
|
|
1705
2214
|
}
|
|
1706
|
-
if (url.includes('/api/taskforce/
|
|
1707
|
-
return
|
|
2215
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2216
|
+
return jsonResponse({
|
|
2217
|
+
plans: [{
|
|
2218
|
+
planId: 'pro',
|
|
2219
|
+
displayName: 'Pro',
|
|
2220
|
+
status: 'active',
|
|
2221
|
+
defaultVersion: {
|
|
2222
|
+
planVersionId: 'pro-v1',
|
|
2223
|
+
versionNumber: 1,
|
|
2224
|
+
seatLimit: null,
|
|
2225
|
+
metadata: {}
|
|
2226
|
+
},
|
|
2227
|
+
features: [],
|
|
2228
|
+
pricing: {
|
|
2229
|
+
month: {
|
|
2230
|
+
planVersionId: 'pro-v1',
|
|
2231
|
+
interval: 'month',
|
|
2232
|
+
pricingType: 'stripe',
|
|
2233
|
+
stripePriceId: 'price_pro_month',
|
|
2234
|
+
displayAmount: '$19.00',
|
|
2235
|
+
unitAmount: 1900,
|
|
2236
|
+
currency: 'usd'
|
|
2237
|
+
},
|
|
2238
|
+
year: {
|
|
2239
|
+
planVersionId: 'pro-v1',
|
|
2240
|
+
interval: 'year',
|
|
2241
|
+
pricingType: 'stripe',
|
|
2242
|
+
stripePriceId: 'price_pro_year',
|
|
2243
|
+
displayAmount: '$190.00',
|
|
2244
|
+
unitAmount: 19000,
|
|
2245
|
+
currency: 'usd'
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}]
|
|
2249
|
+
});
|
|
1708
2250
|
}
|
|
1709
2251
|
if (url.includes('/api/taskforce/auth/register')) {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
})
|
|
2252
|
+
authenticated = true;
|
|
2253
|
+
return jsonResponse({
|
|
2254
|
+
success: true,
|
|
2255
|
+
workspaceSetupRequired: false,
|
|
2256
|
+
verificationRequired: false,
|
|
2257
|
+
checkoutPending: true,
|
|
2258
|
+
commercialState: 'checkout_pending'
|
|
1718
2259
|
});
|
|
1719
2260
|
}
|
|
1720
|
-
|
|
2261
|
+
if (url.includes('/api/taskforce/billing/checkout-session')) {
|
|
2262
|
+
return jsonResponse({
|
|
2263
|
+
checkoutState: 'updated',
|
|
2264
|
+
message: 'Checkout started.'
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
2268
|
+
return jsonResponse(mockCategories);
|
|
2269
|
+
}
|
|
2270
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2271
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2272
|
+
}
|
|
2273
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
2274
|
+
return jsonResponse({ archived: [] });
|
|
2275
|
+
}
|
|
2276
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
2277
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2278
|
+
}
|
|
2279
|
+
if (url.includes('/api/taskforce/types')) {
|
|
2280
|
+
return jsonResponse({ types: [] });
|
|
2281
|
+
}
|
|
2282
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
2283
|
+
return jsonResponse({ priorities: [] });
|
|
2284
|
+
}
|
|
2285
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
2286
|
+
return jsonResponse({ approaches: [] });
|
|
2287
|
+
}
|
|
2288
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
2289
|
+
return jsonResponse({ specialists: [] });
|
|
2290
|
+
}
|
|
2291
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2292
|
+
return jsonResponse({ templates: [] });
|
|
2293
|
+
}
|
|
2294
|
+
return jsonResponse({});
|
|
1721
2295
|
});
|
|
1722
2296
|
vi.stubGlobal('fetch', fetchMock);
|
|
1723
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
await user.click(screen.getByRole('button', { name: 'Fill Form' }));
|
|
1728
|
-
expect(String(emailInput.value)).toMatch(/^debug\+[a-z0-9]+@example\.com$/);
|
|
1729
|
-
expect(String(displayNameInput.value)).toMatch(/^Debug [A-Z0-9]{4}$/);
|
|
1730
|
-
expect(passwordInput).toHaveValue('DebugPass123!');
|
|
2297
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2298
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
|
|
2299
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
|
|
2300
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1731
2301
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1732
2302
|
await waitFor(() => {
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
expect(
|
|
1737
|
-
|
|
1738
|
-
expect(String(init?.method || '')).toBe('POST');
|
|
1739
|
-
expect(String(init?.credentials || '')).toBe('include');
|
|
1740
|
-
expect(payload.email).toBe(String(emailInput.value));
|
|
1741
|
-
expect(payload.displayName).toBe(String(displayNameInput.value));
|
|
1742
|
-
expect(payload.password).toBe('DebugPass123!');
|
|
1743
|
-
});
|
|
1744
|
-
nowSpy.mockRestore();
|
|
2303
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2304
|
+
});
|
|
2305
|
+
await waitFor(() => {
|
|
2306
|
+
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
|
|
2307
|
+
});
|
|
1745
2308
|
});
|
|
1746
|
-
it('
|
|
2309
|
+
it('shows verification before workspace setup when registration still requires email verification', async () => {
|
|
1747
2310
|
const user = userEvent.setup();
|
|
1748
2311
|
const fetchMock = vi.fn((input) => {
|
|
1749
2312
|
const url = String(input);
|
|
@@ -1752,8 +2315,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1752
2315
|
ok: true,
|
|
1753
2316
|
json: () => Promise.resolve({
|
|
1754
2317
|
authenticated: false,
|
|
1755
|
-
runtimeMode: '
|
|
1756
|
-
authRequiredForApi:
|
|
2318
|
+
runtimeMode: 'cloud',
|
|
2319
|
+
authRequiredForApi: true
|
|
1757
2320
|
})
|
|
1758
2321
|
});
|
|
1759
2322
|
}
|
|
@@ -1762,13 +2325,13 @@ describe('TaskforceCore Integration', () => {
|
|
|
1762
2325
|
ok: true,
|
|
1763
2326
|
json: () => Promise.resolve({
|
|
1764
2327
|
...mockConfig,
|
|
1765
|
-
runtimeMode: '
|
|
1766
|
-
authRequiredForApi:
|
|
2328
|
+
runtimeMode: 'cloud',
|
|
2329
|
+
authRequiredForApi: true,
|
|
1767
2330
|
setupState: {
|
|
1768
2331
|
globalSetupState: 'present',
|
|
1769
2332
|
workspaceSetupState: 'present',
|
|
1770
|
-
runtimeMode: '
|
|
1771
|
-
workspaceId: '
|
|
2333
|
+
runtimeMode: 'cloud',
|
|
2334
|
+
workspaceId: 'default',
|
|
1772
2335
|
mode: 'core',
|
|
1773
2336
|
forceSetup: false,
|
|
1774
2337
|
forceGlobalSetup: false,
|
|
@@ -1790,8 +2353,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1790
2353
|
ok: true,
|
|
1791
2354
|
json: () => Promise.resolve({
|
|
1792
2355
|
success: true,
|
|
1793
|
-
workspaceSetupRequired:
|
|
1794
|
-
verificationRequired:
|
|
2356
|
+
workspaceSetupRequired: true,
|
|
2357
|
+
verificationRequired: true,
|
|
2358
|
+
emailSent: true
|
|
1795
2359
|
})
|
|
1796
2360
|
});
|
|
1797
2361
|
}
|
|
@@ -1804,12 +2368,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1804
2368
|
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1805
2369
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1806
2370
|
await waitFor(() => {
|
|
1807
|
-
expect(screen.getByPlaceholderText('
|
|
2371
|
+
expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
|
|
1808
2372
|
});
|
|
1809
|
-
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
1810
2373
|
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
2374
|
+
expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
|
|
1811
2375
|
});
|
|
1812
|
-
it('allows returning to sign-in/register from forgot mode', async () => {
|
|
2376
|
+
it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
|
|
1813
2377
|
const user = userEvent.setup();
|
|
1814
2378
|
const fetchMock = vi.fn((url) => {
|
|
1815
2379
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -1872,14 +2436,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
1872
2436
|
json: () => Promise.resolve({
|
|
1873
2437
|
success: true,
|
|
1874
2438
|
config: {
|
|
1875
|
-
baseUrl: '
|
|
1876
|
-
apiBaseUrl: '
|
|
1877
|
-
cloudAuthBaseUrl: '
|
|
1878
|
-
runtimeMode: '
|
|
2439
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2440
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2441
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2442
|
+
runtimeMode: 'cloud',
|
|
1879
2443
|
authSource: 'cloud',
|
|
1880
|
-
workspaceMode: '
|
|
1881
|
-
workspaceSwitchingEnabled:
|
|
1882
|
-
cloudAuthViaLocalProxy:
|
|
2444
|
+
workspaceMode: 'multi-cloud',
|
|
2445
|
+
workspaceSwitchingEnabled: true,
|
|
2446
|
+
cloudAuthViaLocalProxy: false
|
|
1883
2447
|
}
|
|
1884
2448
|
})
|
|
1885
2449
|
});
|
|
@@ -1889,8 +2453,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1889
2453
|
ok: true,
|
|
1890
2454
|
json: () => Promise.resolve({
|
|
1891
2455
|
authenticated: false,
|
|
1892
|
-
runtimeMode: '
|
|
1893
|
-
authRequiredForApi:
|
|
2456
|
+
runtimeMode: 'cloud',
|
|
2457
|
+
authRequiredForApi: true
|
|
1894
2458
|
})
|
|
1895
2459
|
});
|
|
1896
2460
|
}
|
|
@@ -1899,12 +2463,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1899
2463
|
ok: true,
|
|
1900
2464
|
json: () => Promise.resolve({
|
|
1901
2465
|
...mockConfig,
|
|
1902
|
-
runtimeMode: '
|
|
1903
|
-
authRequiredForApi:
|
|
2466
|
+
runtimeMode: 'cloud',
|
|
2467
|
+
authRequiredForApi: true,
|
|
1904
2468
|
setupState: {
|
|
1905
2469
|
globalSetupState: 'present',
|
|
1906
2470
|
workspaceSetupState: 'present',
|
|
1907
|
-
runtimeMode: '
|
|
2471
|
+
runtimeMode: 'cloud',
|
|
1908
2472
|
workspaceId: 'workspace-local-active',
|
|
1909
2473
|
mode: 'core',
|
|
1910
2474
|
forceSetup: false,
|
|
@@ -1944,7 +2508,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1944
2508
|
json: () => Promise.resolve({
|
|
1945
2509
|
success: false,
|
|
1946
2510
|
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
1947
|
-
error: 'Configured
|
|
2511
|
+
error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
|
|
1948
2512
|
})
|
|
1949
2513
|
});
|
|
1950
2514
|
}
|
|
@@ -3078,13 +3642,20 @@ describe('TaskforceCore Integration', () => {
|
|
|
3078
3642
|
});
|
|
3079
3643
|
expect(cloudWorkspaceCreated).toBe(false);
|
|
3080
3644
|
});
|
|
3081
|
-
it('shows local runtime Sign In
|
|
3645
|
+
it('shows local runtime Sign In action and routes to shared login', async () => {
|
|
3082
3646
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3083
3647
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3084
|
-
expect(standaloneLayoutSource).toContain('Sign In
|
|
3648
|
+
expect(standaloneLayoutSource).toContain('Sign In');
|
|
3085
3649
|
expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
|
|
3086
3650
|
expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
|
|
3087
3651
|
});
|
|
3652
|
+
it('keeps the account badge neutral until the authenticated identity is fully resolved', () => {
|
|
3653
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3654
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3655
|
+
expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;");
|
|
3656
|
+
expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountAvatar = isAuthenticated && authSessionResolved && accountAvatarUrl.length > 0;");
|
|
3657
|
+
expect(standaloneLayoutSource).toContain('<User size={14} className={accountStyles.avatarIcon} />');
|
|
3658
|
+
});
|
|
3088
3659
|
it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
|
|
3089
3660
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3090
3661
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
@@ -3092,16 +3663,25 @@ describe('TaskforceCore Integration', () => {
|
|
|
3092
3663
|
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3093
3664
|
expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
|
|
3094
3665
|
expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
|
|
3095
|
-
expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
|
|
3096
|
-
expect(standaloneLayoutSource).toContain(
|
|
3666
|
+
expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(accountProfileSummary?.workspaceId || authWorkspaceId || currentWorkspaceId || '').trim();");
|
|
3667
|
+
expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
3668
|
+
expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
|
|
3097
3669
|
expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
|
|
3098
3670
|
});
|
|
3099
3671
|
it('routes local plans and billing UI through the connected cloud billing base', () => {
|
|
3100
3672
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3101
3673
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3674
|
+
const plansPagePath = path.join(process.cwd(), 'src/components/views/PlansPage.tsx');
|
|
3675
|
+
const plansPageSource = fs.readFileSync(plansPagePath, 'utf8');
|
|
3676
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
3677
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3678
|
+
expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
3679
|
+
expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl('/api/taskforce/billing/portal-session')");
|
|
3680
|
+
expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl={resolveCloudAuthUrl}");
|
|
3681
|
+
expect(plansPageSource).toContain('const buildCloudBillingUrl = useCallback((path: string): string => {');
|
|
3682
|
+
expect(plansPageSource).toContain("const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
|
|
3683
|
+
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
|
|
3684
|
+
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
|
|
3105
3685
|
});
|
|
3106
3686
|
it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
|
|
3107
3687
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
@@ -3125,6 +3705,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
3125
3705
|
const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
|
|
3126
3706
|
const appMainSource = fs.readFileSync(appMainPath, 'utf8');
|
|
3127
3707
|
expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
|
|
3708
|
+
expect(useTaskforceSource).toContain('const preferredCloudAuthBaseUrl = isLoopbackHost');
|
|
3709
|
+
expect(useTaskforceSource).toContain("? (runtimeResolvedCloudAuthBaseUrl || explicitConfiguredCloudAuthBaseUrl)");
|
|
3128
3710
|
expect(appMainSource).toContain('resolveClientDeploymentConfig');
|
|
3129
3711
|
expect(appMainSource).toContain('return undefined;');
|
|
3130
3712
|
});
|
|
@@ -3204,6 +3786,110 @@ describe('TaskforceCore Integration', () => {
|
|
|
3204
3786
|
const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
|
|
3205
3787
|
expect(pullCalls.length).toBe(0);
|
|
3206
3788
|
});
|
|
3789
|
+
it('keeps local workspace data loaded after signing out from the account menu', async () => {
|
|
3790
|
+
const user = userEvent.setup();
|
|
3791
|
+
let signedIn = true;
|
|
3792
|
+
const fetchMock = vi.fn((url) => {
|
|
3793
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3794
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
3795
|
+
}
|
|
3796
|
+
if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
|
|
3797
|
+
return jsonResponse({
|
|
3798
|
+
authenticated: signedIn,
|
|
3799
|
+
runtimeMode: 'local',
|
|
3800
|
+
authRequiredForApi: false,
|
|
3801
|
+
email: signedIn ? 'member@example.com' : '',
|
|
3802
|
+
userId: signedIn ? 'user-member' : 'anonymous',
|
|
3803
|
+
workspaceId: 'workspace-local-active',
|
|
3804
|
+
betaAccess: true
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
if (url.includes('https://cloud.example.com/api/taskforce/auth/logout')) {
|
|
3808
|
+
signedIn = false;
|
|
3809
|
+
return jsonResponse({ success: true });
|
|
3810
|
+
}
|
|
3811
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3812
|
+
return jsonResponse({ success: true, state: {} });
|
|
3813
|
+
}
|
|
3814
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3815
|
+
return jsonResponse({
|
|
3816
|
+
success: true,
|
|
3817
|
+
state: {
|
|
3818
|
+
version: 2,
|
|
3819
|
+
enabled: false,
|
|
3820
|
+
phase: 'idle',
|
|
3821
|
+
pullCursor: null,
|
|
3822
|
+
lastPullAt: null,
|
|
3823
|
+
lastPushAt: null,
|
|
3824
|
+
lastSyncedAt: null
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3829
|
+
return jsonResponse({
|
|
3830
|
+
...mockConfig,
|
|
3831
|
+
runtimeMode: 'local',
|
|
3832
|
+
authRequiredForApi: false,
|
|
3833
|
+
workspaceId: 'workspace-local-active',
|
|
3834
|
+
setupState: {
|
|
3835
|
+
globalSetupState: 'present',
|
|
3836
|
+
workspaceSetupState: 'present',
|
|
3837
|
+
runtimeMode: 'local',
|
|
3838
|
+
workspaceId: 'workspace-local-active',
|
|
3839
|
+
mode: 'core',
|
|
3840
|
+
forceSetup: false,
|
|
3841
|
+
forceGlobalSetup: false,
|
|
3842
|
+
forceWorkspaceSetup: false
|
|
3843
|
+
}
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3847
|
+
return jsonResponse({ tasks: mockTasks });
|
|
3848
|
+
}
|
|
3849
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
3850
|
+
return jsonResponse({ archived: [] });
|
|
3851
|
+
}
|
|
3852
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
3853
|
+
return jsonResponse(mockCategories);
|
|
3854
|
+
}
|
|
3855
|
+
if (url.includes('/api/taskforce/types')) {
|
|
3856
|
+
return jsonResponse({ types: [] });
|
|
3857
|
+
}
|
|
3858
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
3859
|
+
return jsonResponse({ priorities: [] });
|
|
3860
|
+
}
|
|
3861
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
3862
|
+
return jsonResponse({ approaches: [] });
|
|
3863
|
+
}
|
|
3864
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
3865
|
+
return jsonResponse({ specialists: [] });
|
|
3866
|
+
}
|
|
3867
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3868
|
+
return jsonResponse({ templates: [] });
|
|
3869
|
+
}
|
|
3870
|
+
if (url.includes('/api/taskforce/initiatives')) {
|
|
3871
|
+
return jsonResponse([]);
|
|
3872
|
+
}
|
|
3873
|
+
if (url.includes('/api/taskforce/workstreams')) {
|
|
3874
|
+
return jsonResponse([]);
|
|
3875
|
+
}
|
|
3876
|
+
return jsonResponse({});
|
|
3877
|
+
});
|
|
3878
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3879
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3880
|
+
await waitFor(() => {
|
|
3881
|
+
expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
|
|
3882
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3883
|
+
});
|
|
3884
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
3885
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
|
|
3886
|
+
await waitFor(() => {
|
|
3887
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
|
|
3888
|
+
expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
|
|
3889
|
+
});
|
|
3890
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
3891
|
+
expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
|
|
3892
|
+
});
|
|
3207
3893
|
it('does not pull from cloud when workspace sync toggle is disabled', async () => {
|
|
3208
3894
|
const fetchMock = vi.fn((url) => {
|
|
3209
3895
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -4745,13 +5431,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
4745
5431
|
expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
|
|
4746
5432
|
});
|
|
4747
5433
|
});
|
|
4748
|
-
it('opens edit profile from the account menu and saves the updated display name', async () => {
|
|
5434
|
+
it('opens edit profile from the account menu and saves the updated display name and avatar', async () => {
|
|
4749
5435
|
const user = userEvent.setup();
|
|
4750
5436
|
let savedDisplayName = 'Original Name';
|
|
5437
|
+
let savedAvatarUrl = '';
|
|
4751
5438
|
const fetchMock = vi.fn((input, init) => {
|
|
4752
5439
|
const url = String(input);
|
|
4753
5440
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4754
|
-
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: '
|
|
5441
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
|
|
4755
5442
|
}
|
|
4756
5443
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
4757
5444
|
return jsonResponse({
|
|
@@ -4761,6 +5448,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
4761
5448
|
userId: 'user-1',
|
|
4762
5449
|
email: 'user@example.com',
|
|
4763
5450
|
displayName: savedDisplayName,
|
|
5451
|
+
avatarUrl: savedAvatarUrl,
|
|
4764
5452
|
workspaceId: 'workspace-local-active'
|
|
4765
5453
|
});
|
|
4766
5454
|
}
|
|
@@ -4817,15 +5505,38 @@ describe('TaskforceCore Integration', () => {
|
|
|
4817
5505
|
return jsonResponse({ templates: [] });
|
|
4818
5506
|
if (url.includes('/api/taskforce/workspace/assignee-options'))
|
|
4819
5507
|
return jsonResponse({ options: [] });
|
|
5508
|
+
if (url.includes('/api/taskforce/auth/profile/avatar/init')) {
|
|
5509
|
+
return jsonResponse({
|
|
5510
|
+
success: true,
|
|
5511
|
+
draftId: 'avatar-draft-1',
|
|
5512
|
+
uploadUrl: 'https://upload.example.com/avatar-draft-1',
|
|
5513
|
+
relativePath: 'users/user-1/profile/avatar/avatar-draft-1-avatar.png',
|
|
5514
|
+
path: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
if (url.includes('https://upload.example.com/avatar-draft-1')) {
|
|
5518
|
+
return Promise.resolve({ ok: true, status: 200, json: async () => ({}) });
|
|
5519
|
+
}
|
|
5520
|
+
if (url.includes('/api/taskforce/auth/profile/avatar/finalize')) {
|
|
5521
|
+
return jsonResponse({
|
|
5522
|
+
success: true,
|
|
5523
|
+
draftId: 'avatar-draft-1',
|
|
5524
|
+
avatarUrl: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
|
|
5525
|
+
});
|
|
5526
|
+
}
|
|
4820
5527
|
if (url.includes('/api/taskforce/auth/profile')) {
|
|
4821
5528
|
const body = JSON.parse(String(init?.body || '{}'));
|
|
4822
5529
|
savedDisplayName = String(body?.displayName || '').trim() || savedDisplayName;
|
|
5530
|
+
if (body?.avatarDraftId === 'avatar-draft-1') {
|
|
5531
|
+
savedAvatarUrl = '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png';
|
|
5532
|
+
}
|
|
4823
5533
|
return jsonResponse({
|
|
4824
5534
|
success: true,
|
|
4825
5535
|
profile: {
|
|
4826
5536
|
userId: 'user-1',
|
|
4827
5537
|
email: 'user@example.com',
|
|
4828
|
-
displayName: savedDisplayName
|
|
5538
|
+
displayName: savedDisplayName,
|
|
5539
|
+
avatarUrl: savedAvatarUrl
|
|
4829
5540
|
}
|
|
4830
5541
|
});
|
|
4831
5542
|
}
|
|
@@ -4845,17 +5556,24 @@ describe('TaskforceCore Integration', () => {
|
|
|
4845
5556
|
const displayNameInput = within(modal).getByPlaceholderText('Display name');
|
|
4846
5557
|
await user.clear(displayNameInput);
|
|
4847
5558
|
await user.type(displayNameInput, 'Updated Name');
|
|
5559
|
+
const avatarInput = within(modal).getByLabelText('Upload profile photo');
|
|
5560
|
+
await user.upload(avatarInput, new File(['avatar'], 'avatar.png', { type: 'image/png' }));
|
|
4848
5561
|
await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
|
|
4849
5562
|
await waitFor(() => {
|
|
4850
|
-
const profileCall = fetchMock.mock.calls.find((call) =>
|
|
5563
|
+
const profileCall = fetchMock.mock.calls.find((call) => {
|
|
5564
|
+
const url = String(call[0]);
|
|
5565
|
+
return url.includes('/api/taskforce/auth/profile') && !url.includes('/api/taskforce/auth/profile/avatar/');
|
|
5566
|
+
});
|
|
4851
5567
|
expect(profileCall).toBeTruthy();
|
|
4852
5568
|
const payload = JSON.parse(String(profileCall?.[1]?.body || '{}'));
|
|
4853
5569
|
expect(payload.displayName).toBe('Updated Name');
|
|
5570
|
+
expect(payload.avatarDraftId).toBe('avatar-draft-1');
|
|
4854
5571
|
});
|
|
4855
5572
|
await waitFor(() => {
|
|
4856
5573
|
expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
|
|
4857
5574
|
});
|
|
4858
5575
|
await user.click(screen.getByTitle(/Account/i));
|
|
4859
5576
|
expect(await screen.findByText('Updated Name')).toBeInTheDocument();
|
|
5577
|
+
expect(screen.getByTitle(/Account/i).querySelector('img')).toHaveAttribute('src', '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png');
|
|
4860
5578
|
});
|
|
4861
5579
|
});
|