@taskforcehq/taskforce 0.3.170 → 0.3.300

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