@taskforcehq/taskforce 0.3.309 → 0.3.313

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 (470) hide show
  1. package/README.md +89 -127
  2. package/dist/TaskforceCore.js +1 -1
  3. package/dist/components/context/ContextAttachmentManager.js +4 -3
  4. package/dist/components/features/InitiativesModule.js +1 -1
  5. package/dist/components/features/TaskSettings.js +1 -1
  6. package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
  7. package/dist/components/ui/DisclosureSectionHeader.js +8 -0
  8. package/dist/components/views/PlanComparisonPage.js +1 -1
  9. package/dist/components/views/StandaloneLayout.js +18 -4
  10. package/dist/components/views/panels/PlanningDrawer.js +2 -1
  11. package/dist/components/views/standalone/modals/AccountHubModal.js +1 -1
  12. package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
  13. package/dist/components/views/standalone/modals/HelpModal.js +17 -2
  14. package/dist/core/PlanEntitlementService.d.ts +2 -0
  15. package/dist/core/PlanEntitlementService.js +57 -4
  16. package/dist/hooks/useFilterSort.js +1 -1
  17. package/dist/hooks/useTaskforce.js +2 -2
  18. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +5 -2
  19. package/dist/localization/locales/en-US.js +1 -1
  20. package/dist/localization/locales/es-419.js +1 -1
  21. package/dist/localization/locales/pt-BR.js +1 -1
  22. package/dist/mcp/runtime.js +49 -14
  23. package/dist/server/routes/billing.js +0 -1
  24. package/dist/ui/assets/AgentsModule-BULqIo8e.js +1 -0
  25. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-DHr4rHK9.js} +1 -1
  26. package/dist/ui/assets/ContextAttachmentManager-DoMe2OpY.js +3 -0
  27. package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-DumBWhvb.js} +1 -1
  28. package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-sHk8Nzbq.js} +1 -1
  29. package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-3X9UNzHh.js} +1 -1
  30. package/dist/ui/assets/PlansPage-C3A3EYaV.js +1 -0
  31. package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
  32. package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-C8G7pWkX.js} +1 -1
  33. package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DM1o9Wla.js} +2 -2
  34. package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CW0crzvD.js} +1 -1
  35. package/dist/ui/assets/documentReferences-BZB3nMRb.js +1 -0
  36. package/dist/ui/assets/index-BWFQ9mGn.js +5 -0
  37. package/dist/ui/assets/{index-BvAbqx5Q.css → index-DcK_8tuE.css} +1 -1
  38. package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
  39. package/dist/ui/index.html +3 -3
  40. package/dist/utils/supportContact.d.ts +9 -0
  41. package/dist/utils/supportContact.js +32 -0
  42. package/package.json +12 -4
  43. package/scripts/postinstall.js +4 -3
  44. package/dist/TaskforceCore.CategoryFallback.test.d.ts +0 -1
  45. package/dist/TaskforceCore.CategoryFallback.test.js +0 -101
  46. package/dist/TaskforceCore.filter-persistence.test.d.ts +0 -1
  47. package/dist/TaskforceCore.filter-persistence.test.js +0 -402
  48. package/dist/TaskforceCore.routing.test.d.ts +0 -1
  49. package/dist/TaskforceCore.routing.test.js +0 -311
  50. package/dist/TaskforceCore.test.d.ts +0 -1
  51. package/dist/TaskforceCore.test.js +0 -7910
  52. package/dist/TaskforceWidget.test.d.ts +0 -1
  53. package/dist/TaskforceWidget.test.js +0 -92
  54. package/dist/UnsavedChangesRelaxed.test.d.ts +0 -1
  55. package/dist/UnsavedChangesRelaxed.test.js +0 -16
  56. package/dist/__tests__/localizationCatalogs.test.d.ts +0 -1
  57. package/dist/__tests__/localizationCatalogs.test.js +0 -54
  58. package/dist/__tests__/workflowSimulation.test.d.ts +0 -1
  59. package/dist/__tests__/workflowSimulation.test.js +0 -113
  60. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts +0 -1
  61. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +0 -278
  62. package/dist/annotatedAttachments/payload.test.d.ts +0 -1
  63. package/dist/annotatedAttachments/payload.test.js +0 -67
  64. package/dist/compat/workspaceSyncCompat.test.d.ts +0 -1
  65. package/dist/compat/workspaceSyncCompat.test.js +0 -124
  66. package/dist/components/features/AgentsModule.test.d.ts +0 -1
  67. package/dist/components/features/AgentsModule.test.js +0 -601
  68. package/dist/components/features/AnnotatedAttachmentWorkspace.test.d.ts +0 -1
  69. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +0 -2883
  70. package/dist/components/features/DocumentWorkspace.test.d.ts +0 -1
  71. package/dist/components/features/DocumentWorkspace.test.js +0 -1439
  72. package/dist/components/features/TaskSettings.test.d.ts +0 -1
  73. package/dist/components/features/TaskSettings.test.js +0 -1674
  74. package/dist/components/features/settings/TaxonomyLibraryManager.test.d.ts +0 -1
  75. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +0 -141
  76. package/dist/components/task/TaskActionHeader.test.d.ts +0 -1
  77. package/dist/components/task/TaskActionHeader.test.js +0 -91
  78. package/dist/components/task/TaskCard.test.d.ts +0 -1
  79. package/dist/components/task/TaskCard.test.js +0 -513
  80. package/dist/components/task/TaskContextUpload.test.d.ts +0 -1
  81. package/dist/components/task/TaskContextUpload.test.js +0 -506
  82. package/dist/components/task/TaskForm.test.d.ts +0 -1
  83. package/dist/components/task/TaskForm.test.js +0 -946
  84. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +0 -1
  85. package/dist/components/task/TaskKanban.empty-column-drop.test.js +0 -66
  86. package/dist/components/task/TaskKanban.planning-drop.test.d.ts +0 -1
  87. package/dist/components/task/TaskKanban.planning-drop.test.js +0 -93
  88. package/dist/components/task/TaskKanban.test.d.ts +0 -1
  89. package/dist/components/task/TaskKanban.test.js +0 -99
  90. package/dist/components/task/TaskList.test.d.ts +0 -1
  91. package/dist/components/task/TaskList.test.js +0 -473
  92. package/dist/components/task/TaskStatusActions.test.d.ts +0 -1
  93. package/dist/components/task/TaskStatusActions.test.js +0 -54
  94. package/dist/components/task/taskKanbanDropRules.test.d.ts +0 -1
  95. package/dist/components/task/taskKanbanDropRules.test.js +0 -114
  96. package/dist/components/ui/LevelSelector.test.d.ts +0 -1
  97. package/dist/components/ui/LevelSelector.test.js +0 -55
  98. package/dist/components/ui/Markdown.test.d.ts +0 -1
  99. package/dist/components/ui/Markdown.test.js +0 -38
  100. package/dist/components/ui/MultiSelectFilter.test.d.ts +0 -1
  101. package/dist/components/ui/MultiSelectFilter.test.js +0 -74
  102. package/dist/components/ui/TaxonomyDropdown.test.d.ts +0 -1
  103. package/dist/components/ui/TaxonomyDropdown.test.js +0 -130
  104. package/dist/components/ui/TopNoticeLayer.test.d.ts +0 -1
  105. package/dist/components/ui/TopNoticeLayer.test.js +0 -25
  106. package/dist/components/views/AppShellHeader.test.d.ts +0 -1
  107. package/dist/components/views/AppShellHeader.test.js +0 -42
  108. package/dist/components/views/PlanComparisonPage.test.d.ts +0 -1
  109. package/dist/components/views/PlanComparisonPage.test.js +0 -757
  110. package/dist/components/views/PlansPage.test.d.ts +0 -1
  111. package/dist/components/views/PlansPage.test.js +0 -710
  112. package/dist/components/views/chrome/WorkspaceChromeSections.test.d.ts +0 -1
  113. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +0 -54
  114. package/dist/components/views/chrome/workspaceChrome.test.d.ts +0 -1
  115. package/dist/components/views/chrome/workspaceChrome.test.js +0 -52
  116. package/dist/components/views/panels/DocumentFilterToolbar.test.d.ts +0 -1
  117. package/dist/components/views/panels/DocumentFilterToolbar.test.js +0 -19
  118. package/dist/components/views/panels/FilterToolbar.test.d.ts +0 -1
  119. package/dist/components/views/panels/FilterToolbar.test.js +0 -47
  120. package/dist/components/views/panels/PlanningDrawer.test.d.ts +0 -1
  121. package/dist/components/views/panels/PlanningDrawer.test.js +0 -529
  122. package/dist/components/views/planningScope.test.d.ts +0 -1
  123. package/dist/components/views/planningScope.test.js +0 -60
  124. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +0 -1
  125. package/dist/components/views/standalone/modals/AccountHubModal.test.js +0 -66
  126. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +0 -1
  127. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +0 -24
  128. package/dist/components/views/teamManagementAccess.test.d.ts +0 -1
  129. package/dist/components/views/teamManagementAccess.test.js +0 -44
  130. package/dist/config/deployment.test.d.ts +0 -1
  131. package/dist/config/deployment.test.js +0 -90
  132. package/dist/config/envSchema.test.d.ts +0 -1
  133. package/dist/config/envSchema.test.js +0 -125
  134. package/dist/core/AiProfileService.test.d.ts +0 -1
  135. package/dist/core/AiProfileService.test.js +0 -184
  136. package/dist/core/AiProfiles.test.d.ts +0 -1
  137. package/dist/core/AiProfiles.test.js +0 -1873
  138. package/dist/core/AuthTokenService.test.d.ts +0 -1
  139. package/dist/core/AuthTokenService.test.js +0 -78
  140. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +0 -1
  141. package/dist/core/BulkUpdateFieldsPreview.test.js +0 -62
  142. package/dist/core/CompletedAt.test.d.ts +0 -1
  143. package/dist/core/CompletedAt.test.js +0 -44
  144. package/dist/core/CreatedBy.test.d.ts +0 -1
  145. package/dist/core/CreatedBy.test.js +0 -188
  146. package/dist/core/DeletedTaskTrash.test.d.ts +0 -1
  147. package/dist/core/DeletedTaskTrash.test.js +0 -104
  148. package/dist/core/EntitlementsPolicy.test.d.ts +0 -1
  149. package/dist/core/EntitlementsPolicy.test.js +0 -280
  150. package/dist/core/GlobalSettingsService.test.d.ts +0 -1
  151. package/dist/core/GlobalSettingsService.test.js +0 -45
  152. package/dist/core/JsonBackupRuntime.test.d.ts +0 -1
  153. package/dist/core/JsonBackupRuntime.test.js +0 -155
  154. package/dist/core/PlanFeatureCatalog.test.d.ts +0 -1
  155. package/dist/core/PlanFeatureCatalog.test.js +0 -139
  156. package/dist/core/PlanVersionPolicy.test.d.ts +0 -1
  157. package/dist/core/PlanVersionPolicy.test.js +0 -204
  158. package/dist/core/PlanningEntities.test.d.ts +0 -1
  159. package/dist/core/PlanningEntities.test.js +0 -509
  160. package/dist/core/ScheduleFields.test.d.ts +0 -1
  161. package/dist/core/ScheduleFields.test.js +0 -193
  162. package/dist/core/SetupState.test.d.ts +0 -1
  163. package/dist/core/SetupState.test.js +0 -113
  164. package/dist/core/SignupMonetizationSettings.test.d.ts +0 -1
  165. package/dist/core/SignupMonetizationSettings.test.js +0 -515
  166. package/dist/core/Stats.test.d.ts +0 -1
  167. package/dist/core/Stats.test.js +0 -36
  168. package/dist/core/SystemAdmin.test.d.ts +0 -1
  169. package/dist/core/SystemAdmin.test.js +0 -1106
  170. package/dist/core/TaskActivitySync.test.d.ts +0 -1
  171. package/dist/core/TaskActivitySync.test.js +0 -90
  172. package/dist/core/TaskAttachmentsCanonical.test.d.ts +0 -1
  173. package/dist/core/TaskAttachmentsCanonical.test.js +0 -356
  174. package/dist/core/TaskAuditTimeline.test.d.ts +0 -1
  175. package/dist/core/TaskAuditTimeline.test.js +0 -319
  176. package/dist/core/TaskTaxonomyValidation.test.d.ts +0 -1
  177. package/dist/core/TaskTaxonomyValidation.test.js +0 -440
  178. package/dist/core/Taskforce.ids.test.d.ts +0 -1
  179. package/dist/core/Taskforce.ids.test.js +0 -611
  180. package/dist/core/Taskforce.listTasksSlim.test.d.ts +0 -1
  181. package/dist/core/Taskforce.listTasksSlim.test.js +0 -91
  182. package/dist/core/Taskforce.mcpAuth.test.d.ts +0 -1
  183. package/dist/core/Taskforce.mcpAuth.test.js +0 -450
  184. package/dist/core/Taskforce.realtimeMutation.test.d.ts +0 -1
  185. package/dist/core/Taskforce.realtimeMutation.test.js +0 -131
  186. package/dist/core/UserProfileAvatarDrafts.test.d.ts +0 -1
  187. package/dist/core/UserProfileAvatarDrafts.test.js +0 -138
  188. package/dist/core/WorkspaceDelete.test.d.ts +0 -1
  189. package/dist/core/WorkspaceDelete.test.js +0 -159
  190. package/dist/core/WorkspacePermissions.test.d.ts +0 -1
  191. package/dist/core/WorkspacePermissions.test.js +0 -270
  192. package/dist/core/WorkspacePlanMode.test.d.ts +0 -1
  193. package/dist/core/WorkspacePlanMode.test.js +0 -383
  194. package/dist/core/WorkspaceSyncSettings.test.d.ts +0 -1
  195. package/dist/core/WorkspaceSyncSettings.test.js +0 -65
  196. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +0 -1
  197. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +0 -212
  198. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +0 -1
  199. package/dist/documentReviews/DocumentReviewSessionStore.test.js +0 -127
  200. package/dist/hooks/authGuardPolicy.test.d.ts +0 -1
  201. package/dist/hooks/authGuardPolicy.test.js +0 -76
  202. package/dist/hooks/bootstrapTimeouts.test.d.ts +0 -1
  203. package/dist/hooks/bootstrapTimeouts.test.js +0 -12
  204. package/dist/hooks/sync/controlPlane.test.d.ts +0 -1
  205. package/dist/hooks/sync/controlPlane.test.js +0 -121
  206. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +0 -1
  207. package/dist/hooks/sync/lifecyclePolicy.test.js +0 -124
  208. package/dist/hooks/sync/orchestratorShared.test.d.ts +0 -1
  209. package/dist/hooks/sync/orchestratorShared.test.js +0 -98
  210. package/dist/hooks/sync/pullLifecycle.test.d.ts +0 -1
  211. package/dist/hooks/sync/pullLifecycle.test.js +0 -80
  212. package/dist/hooks/sync/recovery.test.d.ts +0 -1
  213. package/dist/hooks/sync/recovery.test.js +0 -292
  214. package/dist/hooks/sync/transfers.test.d.ts +0 -1
  215. package/dist/hooks/sync/transfers.test.js +0 -928
  216. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +0 -1
  217. package/dist/hooks/sync/useRecentSyncEvents.test.js +0 -124
  218. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +0 -1
  219. package/dist/hooks/sync/useSyncStatusActions.test.js +0 -133
  220. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +0 -1
  221. package/dist/hooks/sync/useSyncStatusControls.test.js +0 -88
  222. package/dist/hooks/useFilterSort.test.d.ts +0 -1
  223. package/dist/hooks/useFilterSort.test.js +0 -419
  224. package/dist/hooks/useRealtimeSync.test.d.ts +0 -1
  225. package/dist/hooks/useRealtimeSync.test.js +0 -311
  226. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +0 -1
  227. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +0 -54
  228. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +0 -1
  229. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +0 -228
  230. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +0 -1
  231. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +0 -2027
  232. package/dist/hooks/useTaskData.test.d.ts +0 -1
  233. package/dist/hooks/useTaskData.test.js +0 -291
  234. package/dist/hooks/useTaskMutations.test.d.ts +0 -1
  235. package/dist/hooks/useTaskMutations.test.js +0 -256
  236. package/dist/hooks/useTaskforce.contract.test.d.ts +0 -1
  237. package/dist/hooks/useTaskforce.contract.test.js +0 -7
  238. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +0 -1
  239. package/dist/hooks/useTaskforce.priority-filters.test.js +0 -28
  240. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +0 -1
  241. package/dist/hooks/useTaskforce.runtime-routing.test.js +0 -246
  242. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +0 -1
  243. package/dist/hooks/useTaskforce.sync-behavior.test.js +0 -3644
  244. package/dist/hooks/useWorkspaceSyncController.test.d.ts +0 -1
  245. package/dist/hooks/useWorkspaceSyncController.test.js +0 -192
  246. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +0 -1
  247. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +0 -77
  248. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +0 -1
  249. package/dist/hooks/workspace/workspaceLocalState.test.js +0 -59
  250. package/dist/localization/localizationCatalogs.test.d.ts +0 -1
  251. package/dist/localization/localizationCatalogs.test.js +0 -37
  252. package/dist/mcp/aiProfileBootstrap.test.d.ts +0 -1
  253. package/dist/mcp/aiProfileBootstrap.test.js +0 -68
  254. package/dist/mcp/canonicalAssetHelpers.test.d.ts +0 -1
  255. package/dist/mcp/canonicalAssetHelpers.test.js +0 -67
  256. package/dist/mcp/clientIntegrations.test.d.ts +0 -1
  257. package/dist/mcp/clientIntegrations.test.js +0 -219
  258. package/dist/mcp/optionResolution.test.d.ts +0 -1
  259. package/dist/mcp/optionResolution.test.js +0 -43
  260. package/dist/mcp/runtime.test.d.ts +0 -1
  261. package/dist/mcp/runtime.test.js +0 -5120
  262. package/dist/mcp/structuredCommentValidation.test.d.ts +0 -1
  263. package/dist/mcp/structuredCommentValidation.test.js +0 -31
  264. package/dist/mcp/structuredDocValidation.test.d.ts +0 -1
  265. package/dist/mcp/structuredDocValidation.test.js +0 -35
  266. package/dist/mcp/toolRegistry.test.d.ts +0 -1
  267. package/dist/mcp/toolRegistry.test.js +0 -34
  268. package/dist/migrations/billingSchemaParity.test.d.ts +0 -1
  269. package/dist/migrations/billingSchemaParity.test.js +0 -160
  270. package/dist/migrations/taskSchemaMigrations.test.d.ts +0 -1
  271. package/dist/migrations/taskSchemaMigrations.test.js +0 -225
  272. package/dist/qaOpen.test.d.ts +0 -1
  273. package/dist/qaOpen.test.js +0 -56
  274. package/dist/releaseVersion.test.d.ts +0 -1
  275. package/dist/releaseVersion.test.js +0 -32
  276. package/dist/runtime/appGates.test.d.ts +0 -1
  277. package/dist/runtime/appGates.test.js +0 -42
  278. package/dist/server/aiProfileAvatarAssets.test.d.ts +0 -1
  279. package/dist/server/aiProfileAvatarAssets.test.js +0 -213
  280. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +0 -1
  281. package/dist/server/annotatedAttachmentsRoutes.test.js +0 -532
  282. package/dist/server/auth.test.d.ts +0 -1
  283. package/dist/server/auth.test.js +0 -131
  284. package/dist/server/buildInfo.test.d.ts +0 -1
  285. package/dist/server/buildInfo.test.js +0 -29
  286. package/dist/server/cors.test.d.ts +0 -1
  287. package/dist/server/cors.test.js +0 -77
  288. package/dist/server/documentReviewRoutes.test.d.ts +0 -1
  289. package/dist/server/documentReviewRoutes.test.js +0 -190
  290. package/dist/server/email.test.d.ts +0 -1
  291. package/dist/server/email.test.js +0 -113
  292. package/dist/server/index.cookieProxy.test.d.ts +0 -1
  293. package/dist/server/index.cookieProxy.test.js +0 -67
  294. package/dist/server/index.rateLimit.test.d.ts +0 -1
  295. package/dist/server/index.rateLimit.test.js +0 -22
  296. package/dist/server/index.test.d.ts +0 -1
  297. package/dist/server/index.test.js +0 -283
  298. package/dist/server/rateLimit.test.d.ts +0 -1
  299. package/dist/server/rateLimit.test.js +0 -107
  300. package/dist/server/realtimeSync.auth.test.d.ts +0 -1
  301. package/dist/server/realtimeSync.auth.test.js +0 -62
  302. package/dist/server/realtimeSync.replay.test.d.ts +0 -1
  303. package/dist/server/realtimeSync.replay.test.js +0 -192
  304. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +0 -1
  305. package/dist/server/realtimeSync.workspaceIsolation.test.js +0 -101
  306. package/dist/server/routes/billing.test.d.ts +0 -1
  307. package/dist/server/routes/billing.test.js +0 -3455
  308. package/dist/server/routes/shared.test.d.ts +0 -1
  309. package/dist/server/routes/shared.test.js +0 -32
  310. package/dist/server/routes/sync.integration.test.d.ts +0 -1
  311. package/dist/server/routes/sync.integration.test.js +0 -2932
  312. package/dist/server/routes/syncAuxRoutes.test.d.ts +0 -1
  313. package/dist/server/routes/syncAuxRoutes.test.js +0 -158
  314. package/dist/server/routes/syncRouteGuards.test.d.ts +0 -1
  315. package/dist/server/routes/syncRouteGuards.test.js +0 -94
  316. package/dist/server/routes.test.d.ts +0 -1
  317. package/dist/server/routes.test.js +0 -15612
  318. package/dist/server/runtimeMode.test.d.ts +0 -1
  319. package/dist/server/runtimeMode.test.js +0 -26
  320. package/dist/server/securityHeaders.test.d.ts +0 -1
  321. package/dist/server/securityHeaders.test.js +0 -70
  322. package/dist/services/templateEngine.test.d.ts +0 -1
  323. package/dist/services/templateEngine.test.js +0 -150
  324. package/dist/services/templates.test.d.ts +0 -1
  325. package/dist/services/templates.test.js +0 -45
  326. package/dist/services/workflowDocumentFixtures.test.d.ts +0 -1
  327. package/dist/services/workflowDocumentFixtures.test.js +0 -78
  328. package/dist/services/workflowExportMatrix.test.d.ts +0 -1
  329. package/dist/services/workflowExportMatrix.test.js +0 -113
  330. package/dist/services/workflowExportSnapshots.test.d.ts +0 -1
  331. package/dist/services/workflowExportSnapshots.test.js +0 -30
  332. package/dist/services/workflowGeneration.test.d.ts +0 -1
  333. package/dist/services/workflowGeneration.test.js +0 -12
  334. package/dist/services/workflowSimulation.test.d.ts +0 -1
  335. package/dist/services/workflowSimulation.test.js +0 -113
  336. package/dist/services/workflowV2Policy.test.d.ts +0 -1
  337. package/dist/services/workflowV2Policy.test.js +0 -80
  338. package/dist/shared/customTaxonomyLibrary.test.d.ts +0 -1
  339. package/dist/shared/customTaxonomyLibrary.test.js +0 -20
  340. package/dist/shared/passwordPolicy.test.d.ts +0 -1
  341. package/dist/shared/passwordPolicy.test.js +0 -34
  342. package/dist/shared/runtimeContract.test.d.ts +0 -1
  343. package/dist/shared/runtimeContract.test.js +0 -23
  344. package/dist/shared/taxonomyLibrary.test.d.ts +0 -1
  345. package/dist/shared/taxonomyLibrary.test.js +0 -26
  346. package/dist/shared/taxonomyState.test.d.ts +0 -1
  347. package/dist/shared/taxonomyState.test.js +0 -71
  348. package/dist/soak/preflight.cli.d.ts +0 -1
  349. package/dist/soak/preflight.cli.js +0 -28
  350. package/dist/soak/preflight.d.ts +0 -26
  351. package/dist/soak/preflight.js +0 -113
  352. package/dist/soak/preflight.test.d.ts +0 -1
  353. package/dist/soak/preflight.test.js +0 -39
  354. package/dist/storage/objectStorageClient.test.d.ts +0 -1
  355. package/dist/storage/objectStorageClient.test.js +0 -82
  356. package/dist/storage/objectStorageConfig.test.d.ts +0 -1
  357. package/dist/storage/objectStorageConfig.test.js +0 -50
  358. package/dist/storage/postgresAdapter.test.d.ts +0 -1
  359. package/dist/storage/postgresAdapter.test.js +0 -29
  360. package/dist/storage/workspaceAssetStore.test.d.ts +0 -1
  361. package/dist/storage/workspaceAssetStore.test.js +0 -588
  362. package/dist/sync/cloudSyncApi.test.d.ts +0 -1
  363. package/dist/sync/cloudSyncApi.test.js +0 -111
  364. package/dist/sync/collaborationSyncPayload.test.d.ts +0 -1
  365. package/dist/sync/collaborationSyncPayload.test.js +0 -125
  366. package/dist/sync/contentSyncPayload.test.d.ts +0 -1
  367. package/dist/sync/contentSyncPayload.test.js +0 -143
  368. package/dist/sync/encryptionService.test.d.ts +0 -1
  369. package/dist/sync/encryptionService.test.js +0 -52
  370. package/dist/sync/planningSyncPayload.test.d.ts +0 -1
  371. package/dist/sync/planningSyncPayload.test.js +0 -92
  372. package/dist/sync/realtimeFeature.test.d.ts +0 -1
  373. package/dist/sync/realtimeFeature.test.js +0 -30
  374. package/dist/sync/realtimeReconcile.test.d.ts +0 -1
  375. package/dist/sync/realtimeReconcile.test.js +0 -43
  376. package/dist/sync/syncApplyHandlers.test.d.ts +0 -1
  377. package/dist/sync/syncApplyHandlers.test.js +0 -626
  378. package/dist/sync/syncService.test.d.ts +0 -1
  379. package/dist/sync/syncService.test.js +0 -903
  380. package/dist/sync/taskSyncPayload.test.d.ts +0 -1
  381. package/dist/sync/taskSyncPayload.test.js +0 -54
  382. package/dist/sync/workspacePullFeed.test.d.ts +0 -1
  383. package/dist/sync/workspacePullFeed.test.js +0 -438
  384. package/dist/sync/workspaceSyncModel.test.d.ts +0 -1
  385. package/dist/sync/workspaceSyncModel.test.js +0 -1442
  386. package/dist/test/setup.d.ts +0 -1
  387. package/dist/test/setup.js +0 -15
  388. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
  389. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
  390. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
  391. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
  392. package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
  393. package/dist/ui/assets/index-MAHKvFqp.js +0 -4
  394. package/dist/utils/accountProfileSummaryCache.test.d.ts +0 -1
  395. package/dist/utils/accountProfileSummaryCache.test.js +0 -63
  396. package/dist/utils/annotatedAttachments.test.d.ts +0 -1
  397. package/dist/utils/annotatedAttachments.test.js +0 -108
  398. package/dist/utils/assignees.test.d.ts +0 -1
  399. package/dist/utils/assignees.test.js +0 -25
  400. package/dist/utils/avatarUpload.test.d.ts +0 -1
  401. package/dist/utils/avatarUpload.test.js +0 -40
  402. package/dist/utils/canonicalAssetKind.test.d.ts +0 -1
  403. package/dist/utils/canonicalAssetKind.test.js +0 -24
  404. package/dist/utils/coalescedRefresh.test.d.ts +0 -1
  405. package/dist/utils/coalescedRefresh.test.js +0 -30
  406. package/dist/utils/constants.icons.test.d.ts +0 -1
  407. package/dist/utils/constants.icons.test.js +0 -41
  408. package/dist/utils/planParser.test.d.ts +0 -1
  409. package/dist/utils/planParser.test.js +0 -183
  410. package/dist/utils/planningReferences.test.d.ts +0 -1
  411. package/dist/utils/planningReferences.test.js +0 -17
  412. package/dist/utils/syncEventDetails.test.d.ts +0 -1
  413. package/dist/utils/syncEventDetails.test.js +0 -23
  414. package/dist/utils/syncEventPresentation.test.d.ts +0 -1
  415. package/dist/utils/syncEventPresentation.test.js +0 -64
  416. package/dist/utils/syncStatusPresentation.test.d.ts +0 -1
  417. package/dist/utils/syncStatusPresentation.test.js +0 -164
  418. package/dist/utils/taskActivity.test.d.ts +0 -1
  419. package/dist/utils/taskActivity.test.js +0 -218
  420. package/dist/utils/taskDraftDefaults.test.d.ts +0 -1
  421. package/dist/utils/taskDraftDefaults.test.js +0 -34
  422. package/dist/utils/taskDragZones.test.d.ts +0 -1
  423. package/dist/utils/taskDragZones.test.js +0 -14
  424. package/dist/utils/taskNormalization.test.d.ts +0 -1
  425. package/dist/utils/taskNormalization.test.js +0 -158
  426. package/dist/utils/taskSearch.test.d.ts +0 -1
  427. package/dist/utils/taskSearch.test.js +0 -69
  428. package/dist/utils/taskUtils.test.d.ts +0 -1
  429. package/dist/utils/taskUtils.test.js +0 -19
  430. package/dist/utils/taskforceApiClient.test.d.ts +0 -1
  431. package/dist/utils/taskforceApiClient.test.js +0 -44
  432. package/dist/utils/taxonomyFormDefaults.test.d.ts +0 -1
  433. package/dist/utils/taxonomyFormDefaults.test.js +0 -63
  434. package/dist/utils/uiNotice.test.d.ts +0 -1
  435. package/dist/utils/uiNotice.test.js +0 -18
  436. package/dist/utils/workspaceIdentity.test.d.ts +0 -1
  437. package/dist/utils/workspaceIdentity.test.js +0 -61
  438. package/dist/utils/workspaceLifecycle.test.d.ts +0 -1
  439. package/dist/utils/workspaceLifecycle.test.js +0 -64
  440. package/dist/utils/workspaceSyncPresentation.test.d.ts +0 -1
  441. package/dist/utils/workspaceSyncPresentation.test.js +0 -71
  442. package/scripts/bootstrap-playwright-auth.mjs +0 -155
  443. package/scripts/check-cloud-mcp.mjs +0 -83
  444. package/scripts/check-compat-boundaries.mjs +0 -41
  445. package/scripts/check-env.ts +0 -31
  446. package/scripts/check-localization-strings.mjs +0 -49
  447. package/scripts/check-runtime-boundaries.mjs +0 -44
  448. package/scripts/debug-global.mjs +0 -34
  449. package/scripts/debug-login.mjs +0 -32
  450. package/scripts/debug-server.mjs +0 -36
  451. package/scripts/deploy-prod.sh +0 -41
  452. package/scripts/deploy-staging.sh +0 -23
  453. package/scripts/export-sqlite-to-postgres.mjs +0 -123
  454. package/scripts/generate-env-example.ts +0 -8
  455. package/scripts/mcp-stdio-adapter.mjs +0 -160
  456. package/scripts/migrate-task-data-assets.mjs +0 -408
  457. package/scripts/playwright-auth.sh +0 -19
  458. package/scripts/qa-open-lib.mjs +0 -175
  459. package/scripts/qa-open-lib.mjs.d.ts +0 -49
  460. package/scripts/qa-open.mjs +0 -352
  461. package/scripts/release-version-lib.mjs +0 -88
  462. package/scripts/release-version.mjs +0 -72
  463. package/scripts/run-billing-performance-smoke.sh +0 -24
  464. package/scripts/run-dev-proxy.sh +0 -51
  465. package/scripts/run-e2e-realtime.sh +0 -30
  466. package/scripts/run-smoke.sh +0 -35
  467. package/scripts/run-soak.sh +0 -41
  468. package/scripts/test-global.js +0 -143
  469. package/scripts/update-homebrew-formula.sh +0 -108
  470. package/scripts/with-esbuild.sh +0 -11
@@ -1,2027 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { act, renderHook, waitFor } from '@testing-library/react';
3
- import { useSyncOrchestrator } from './useSyncOrchestrator';
4
- class MockWebSocket {
5
- static instances = [];
6
- static OPEN = 1;
7
- static CLOSED = 3;
8
- url;
9
- readyState = 0;
10
- sent = [];
11
- listeners = new Map();
12
- constructor(url) {
13
- this.url = url;
14
- MockWebSocket.instances.push(this);
15
- }
16
- close() {
17
- this.readyState = MockWebSocket.CLOSED;
18
- this.emit('close', {});
19
- }
20
- addEventListener(type, listener) {
21
- const bucket = this.listeners.get(type) || new Set();
22
- bucket.add(listener);
23
- this.listeners.set(type, bucket);
24
- }
25
- send(payload) {
26
- if (typeof payload === 'string') {
27
- this.sent.push(payload);
28
- }
29
- }
30
- emit(type, event) {
31
- const bucket = this.listeners.get(type);
32
- if (!bucket)
33
- return;
34
- for (const listener of bucket) {
35
- listener(event);
36
- }
37
- }
38
- open() {
39
- this.readyState = MockWebSocket.OPEN;
40
- this.emit('open', {});
41
- }
42
- message(payload) {
43
- this.emit('message', { data: JSON.stringify(payload) });
44
- }
45
- }
46
- const { ensureCloudWorkspaceReadyForSyncServiceMock, runWorkspacePullSyncServiceMock, runWorkspacePushSyncServiceMock, syncUserGlobalSettingsServiceMock } = vi.hoisted(() => ({
47
- ensureCloudWorkspaceReadyForSyncServiceMock: vi.fn(),
48
- runWorkspacePullSyncServiceMock: vi.fn(),
49
- runWorkspacePushSyncServiceMock: vi.fn(),
50
- syncUserGlobalSettingsServiceMock: vi.fn()
51
- }));
52
- vi.mock('../sync/syncService', async () => {
53
- const actual = await vi.importActual('../sync/syncService');
54
- return {
55
- ...actual,
56
- calculateWorkspaceRetryDelayMs: vi.fn(() => 100),
57
- ensureCloudWorkspaceReadyForSyncService: ensureCloudWorkspaceReadyForSyncServiceMock,
58
- runWorkspacePullSyncService: runWorkspacePullSyncServiceMock,
59
- runWorkspacePushSyncService: runWorkspacePushSyncServiceMock,
60
- syncUserGlobalSettingsService: syncUserGlobalSettingsServiceMock
61
- };
62
- });
63
- function buildInput(overrides) {
64
- return {
65
- currentWorkspaceId: 'workspace-local-active',
66
- cloudAuthConfigured: true,
67
- runtimeMode: 'local',
68
- authSessionResolved: true,
69
- isAuthenticated: true,
70
- authUserId: 'user-1',
71
- projectName: 'Test Project',
72
- resolveCloudAuthUrl: (path) => `https://cloud.example.com${path}`,
73
- resolveWebSocketUrl: (path) => `wss://api.example.com${path}`,
74
- realtimeSyncEnabled: false,
75
- tasks: [],
76
- archivedTasks: [],
77
- initiatives: [],
78
- workstreams: [],
79
- taxonomies: [],
80
- setupState: null,
81
- globalTheme: 'dark',
82
- locale: 'en-US',
83
- globalWeekStartsOn: 'monday',
84
- themeUseGlobalDefault: true,
85
- setTasks: vi.fn(),
86
- setArchivedTasks: vi.fn(),
87
- setAuthBlocked: vi.fn(),
88
- setIsAuthenticated: vi.fn(),
89
- checkAuthSession: vi.fn(async () => true),
90
- setGlobalTheme: vi.fn(),
91
- setCurrentTheme: vi.fn(),
92
- setSetupState: vi.fn(),
93
- setLocale: vi.fn(),
94
- setGlobalWeekStartsOn: vi.fn(),
95
- fetchPlanningEntities: vi.fn(async () => { }),
96
- ...overrides
97
- };
98
- }
99
- describe('useSyncOrchestrator', () => {
100
- beforeEach(() => {
101
- localStorage.clear();
102
- ensureCloudWorkspaceReadyForSyncServiceMock.mockReset();
103
- runWorkspacePullSyncServiceMock.mockReset();
104
- runWorkspacePushSyncServiceMock.mockReset();
105
- syncUserGlobalSettingsServiceMock.mockReset();
106
- ensureCloudWorkspaceReadyForSyncServiceMock.mockResolvedValue({ success: true });
107
- runWorkspacePullSyncServiceMock.mockResolvedValue({
108
- success: true,
109
- syncedAt: '2026-03-15T18:00:00.000Z',
110
- cursor: null,
111
- pages: 0,
112
- appliedChanges: 0,
113
- pullRequestMs: 1,
114
- applyMs: 0,
115
- pulledDeleteTaskIds: new Set(),
116
- pulledActiveUpserts: false,
117
- pulledArchivedUpserts: false,
118
- documentDecryptFailures: []
119
- });
120
- runWorkspacePushSyncServiceMock.mockResolvedValue({
121
- success: true,
122
- syncedAt: '2026-03-15T18:00:00.000Z',
123
- requestMs: 1,
124
- changeCount: 0,
125
- deleteCount: 0,
126
- currentTaskIds: new Set(),
127
- currentInitiativeIds: new Set(),
128
- currentInitiativeWatermarks: new Map(),
129
- currentWorkstreamIds: new Set(),
130
- currentWorkstreamWatermarks: new Map(),
131
- currentAiProfileIds: new Set(),
132
- currentAiProfileWatermarks: new Map(),
133
- currentDocumentPaths: new Set(),
134
- currentDocumentWatermarks: new Map(),
135
- currentAssetPaths: new Set(),
136
- currentAssetWatermarks: new Map(),
137
- currentDocumentReviewSessionIds: new Set(),
138
- currentDocumentReviewSessionWatermarks: new Map(),
139
- currentDocumentReviewCommentIds: new Set(),
140
- currentDocumentReviewCommentWatermarks: new Map(),
141
- currentAnnotatedAttachmentSessionIds: new Set(),
142
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
143
- currentTaskEventWatermarks: new Map(),
144
- deleteTaskIds: new Set(),
145
- pushedWatermarks: new Map(),
146
- emittedEventIds: []
147
- });
148
- syncUserGlobalSettingsServiceMock.mockResolvedValue({
149
- success: true,
150
- mode: 'pulled',
151
- updatedAt: new Date().toISOString()
152
- });
153
- vi.stubGlobal('fetch', vi.fn((url, init) => {
154
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
155
- return Promise.resolve({
156
- ok: true,
157
- json: () => Promise.resolve({
158
- success: true,
159
- state: {
160
- version: 2,
161
- enabled: true,
162
- phase: 'active',
163
- pullCursor: null,
164
- lastPullAt: null,
165
- lastPushAt: null,
166
- lastSyncedAt: null
167
- }
168
- })
169
- });
170
- }
171
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
172
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
173
- }
174
- if (url.includes('/api/taskforce/sync/workspace/documents')) {
175
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
176
- }
177
- if (url.includes('/api/taskforce/sync/workspace/annotated-attachments')) {
178
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
179
- }
180
- if (url.includes('/api/taskforce/sync/events')) {
181
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
182
- }
183
- if (url.includes('/api/taskforce/tasks')) {
184
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
185
- }
186
- if (url.includes('/api/taskforce/archive')) {
187
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
188
- }
189
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
190
- }));
191
- });
192
- afterEach(async () => {
193
- await Promise.resolve();
194
- await Promise.resolve();
195
- vi.useRealTimers();
196
- vi.unstubAllGlobals();
197
- vi.clearAllMocks();
198
- MockWebSocket.instances = [];
199
- });
200
- it('uses the latest retry callback after auth state changes', async () => {
201
- vi.useFakeTimers();
202
- runWorkspacePullSyncServiceMock.mockResolvedValue({
203
- success: false,
204
- kind: 'pull_http',
205
- status: 503,
206
- transient: true,
207
- retryAfterMs: 250,
208
- cursor: null,
209
- pages: 0,
210
- appliedChanges: 0,
211
- pullRequestMs: 1,
212
- applyMs: 0,
213
- pulledDeleteTaskIds: new Set(),
214
- pulledActiveUpserts: false,
215
- pulledArchivedUpserts: false
216
- });
217
- let input = buildInput();
218
- const { result, rerender, unmount } = renderHook((props) => useSyncOrchestrator(props), {
219
- initialProps: input
220
- });
221
- act(() => {
222
- result.current.applyWorkspaceSyncStateSnapshot({
223
- enabled: true,
224
- phase: 'active',
225
- pullCursor: null
226
- });
227
- });
228
- await act(async () => {
229
- await result.current.pullWorkspaceChangesFromCloud();
230
- });
231
- const pullCallsBeforeRetry = runWorkspacePullSyncServiceMock.mock.calls.length;
232
- expect(pullCallsBeforeRetry).toBeGreaterThanOrEqual(1);
233
- expect(result.current.workspaceRetryAt).not.toBeNull();
234
- input = buildInput({
235
- isAuthenticated: false,
236
- checkAuthSession: vi.fn(async () => false)
237
- });
238
- rerender(input);
239
- await act(async () => {
240
- await vi.runOnlyPendingTimersAsync();
241
- });
242
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(pullCallsBeforeRetry);
243
- unmount();
244
- });
245
- it('does not enable realtime until auth session is resolved with a non-anonymous user', async () => {
246
- vi.stubGlobal('WebSocket', MockWebSocket);
247
- const { result, rerender, unmount } = renderHook((props) => useSyncOrchestrator(props), {
248
- initialProps: buildInput({
249
- realtimeSyncEnabled: true,
250
- authSessionResolved: false,
251
- authUserId: 'anonymous'
252
- })
253
- });
254
- act(() => {
255
- result.current.applyWorkspaceSyncStateSnapshot({
256
- enabled: true,
257
- phase: 'active',
258
- pullCursor: null
259
- });
260
- });
261
- expect(MockWebSocket.instances).toHaveLength(0);
262
- rerender(buildInput({
263
- realtimeSyncEnabled: true,
264
- authSessionResolved: true,
265
- authUserId: 'anonymous'
266
- }));
267
- expect(MockWebSocket.instances).toHaveLength(0);
268
- rerender(buildInput({
269
- realtimeSyncEnabled: true,
270
- authSessionResolved: true,
271
- authUserId: 'user-1'
272
- }));
273
- await waitFor(() => {
274
- expect(MockWebSocket.instances).toHaveLength(1);
275
- });
276
- expect(MockWebSocket.instances[0]?.url).toBe('wss://api.example.com/taskforce-ws');
277
- unmount();
278
- });
279
- it('lets a realtime signal break transient retry backoff and pull immediately', async () => {
280
- vi.useFakeTimers();
281
- vi.stubGlobal('WebSocket', MockWebSocket);
282
- runWorkspacePullSyncServiceMock
283
- .mockResolvedValueOnce({
284
- success: false,
285
- kind: 'pull_http',
286
- status: 502,
287
- transient: true,
288
- retryAfterMs: 60_000,
289
- cursor: null,
290
- pages: 0,
291
- appliedChanges: 0,
292
- pullRequestMs: 1,
293
- applyMs: 0,
294
- pulledDeleteTaskIds: new Set(),
295
- pulledActiveUpserts: false,
296
- pulledArchivedUpserts: false,
297
- documentDecryptFailures: []
298
- })
299
- .mockResolvedValueOnce({
300
- success: true,
301
- syncedAt: '2026-03-15T18:05:00.000Z',
302
- cursor: null,
303
- pages: 1,
304
- appliedChanges: 1,
305
- pullRequestMs: 1,
306
- applyMs: 0,
307
- pulledDeleteTaskIds: new Set(),
308
- pulledActiveUpserts: true,
309
- pulledArchivedUpserts: false,
310
- documentDecryptFailures: []
311
- });
312
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
313
- initialProps: buildInput({ realtimeSyncEnabled: true })
314
- });
315
- act(() => {
316
- result.current.applyWorkspaceSyncStateSnapshot({
317
- enabled: true,
318
- phase: 'active',
319
- pullCursor: null
320
- });
321
- });
322
- await act(async () => {
323
- await Promise.resolve();
324
- await Promise.resolve();
325
- });
326
- expect(result.current.workspaceRetryAt).not.toBeNull();
327
- expect(MockWebSocket.instances).toHaveLength(1);
328
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(1);
329
- act(() => {
330
- MockWebSocket.instances[0]?.open();
331
- MockWebSocket.instances[0]?.message({
332
- type: 'taskforce:update',
333
- workspaceId: 'workspace-local-active'
334
- });
335
- });
336
- await act(async () => {
337
- await vi.advanceTimersByTimeAsync(300);
338
- await Promise.resolve();
339
- await Promise.resolve();
340
- });
341
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(2);
342
- expect(result.current.workspaceRetryAt).toBeNull();
343
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T18:05:00.000Z');
344
- unmount();
345
- });
346
- it('refreshes local task collections after event-only task activity pulls', async () => {
347
- const fetchSpy = vi.fn((url, init) => {
348
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
349
- return Promise.resolve({
350
- ok: true,
351
- json: () => Promise.resolve({
352
- success: true,
353
- state: {
354
- version: 2,
355
- enabled: true,
356
- phase: 'active',
357
- pullCursor: null,
358
- lastPullAt: null,
359
- lastPushAt: null,
360
- lastSyncedAt: null
361
- }
362
- })
363
- });
364
- }
365
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
366
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
367
- }
368
- if (url.includes('/api/taskforce/sync/workspace/documents')) {
369
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
370
- }
371
- if (url.includes('/api/taskforce/sync/workspace/annotated-attachments')) {
372
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
373
- }
374
- if (url.includes('/api/taskforce/sync/events')) {
375
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
376
- }
377
- if (url.includes('/api/taskforce/tasks')) {
378
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
379
- }
380
- if (url.includes('/api/taskforce/archive')) {
381
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
382
- }
383
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
384
- });
385
- vi.stubGlobal('fetch', fetchSpy);
386
- runWorkspacePullSyncServiceMock.mockResolvedValueOnce({
387
- success: true,
388
- syncedAt: '2026-03-15T18:06:00.000Z',
389
- cursor: null,
390
- pages: 1,
391
- appliedChanges: 1,
392
- pullRequestMs: 1,
393
- applyMs: 0,
394
- pulledDeleteTaskIds: new Set(),
395
- pulledActiveUpserts: false,
396
- pulledArchivedUpserts: false,
397
- pulledTaskEventUpserts: true,
398
- documentDecryptFailures: []
399
- });
400
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
401
- initialProps: buildInput()
402
- });
403
- act(() => {
404
- result.current.applyWorkspaceSyncStateSnapshot({
405
- enabled: true,
406
- phase: 'active',
407
- pullCursor: null
408
- });
409
- });
410
- await waitFor(() => {
411
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(1);
412
- });
413
- await waitFor(() => {
414
- expect(fetchSpy.mock.calls.some(([url]) => String(url).includes('/api/taskforce/tasks'))).toBe(true);
415
- expect(fetchSpy.mock.calls.some(([url]) => String(url).includes('/api/taskforce/archive'))).toBe(true);
416
- });
417
- unmount();
418
- });
419
- it('marks sync auth as blocked on explicit 401 pull failures', async () => {
420
- const setAuthBlocked = vi.fn();
421
- const setIsAuthenticated = vi.fn();
422
- const checkAuthSession = vi.fn(async () => false);
423
- runWorkspacePullSyncServiceMock.mockResolvedValueOnce({
424
- success: false,
425
- kind: 'pull_http',
426
- status: 401,
427
- transient: false,
428
- retryAfterMs: null,
429
- cursor: null,
430
- pages: 0,
431
- appliedChanges: 0,
432
- pullRequestMs: 1,
433
- applyMs: 0,
434
- pulledDeleteTaskIds: new Set(),
435
- pulledActiveUpserts: false,
436
- pulledArchivedUpserts: false,
437
- documentDecryptFailures: []
438
- });
439
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
440
- initialProps: buildInput({
441
- setAuthBlocked,
442
- setIsAuthenticated,
443
- checkAuthSession
444
- })
445
- });
446
- act(() => {
447
- result.current.applyWorkspaceSyncStateSnapshot({
448
- enabled: true,
449
- phase: 'active',
450
- pullCursor: null
451
- });
452
- });
453
- await waitFor(() => {
454
- expect(setAuthBlocked).toHaveBeenCalledWith(true);
455
- expect(setIsAuthenticated).toHaveBeenCalledWith(false);
456
- expect(checkAuthSession).toHaveBeenCalled();
457
- expect(result.current.workspaceLastErrorMessage).toBe('Session expired. Sign in again to resume cloud sync.');
458
- });
459
- unmount();
460
- });
461
- it('suppresses self-emitted realtime event ids but still pulls on later replay signals', async () => {
462
- vi.useFakeTimers();
463
- vi.stubGlobal('WebSocket', MockWebSocket);
464
- runWorkspacePushSyncServiceMock.mockResolvedValue({
465
- success: true,
466
- syncedAt: '2026-03-15T15:00:00.000Z',
467
- requestMs: 1,
468
- changeCount: 1,
469
- deleteCount: 0,
470
- currentTaskIds: new Set(['task-1']),
471
- currentInitiativeIds: new Set(),
472
- currentInitiativeWatermarks: new Map(),
473
- currentWorkstreamIds: new Set(),
474
- currentWorkstreamWatermarks: new Map(),
475
- currentAiProfileIds: new Set(),
476
- currentAiProfileWatermarks: new Map(),
477
- currentDocumentPaths: new Set(),
478
- currentDocumentWatermarks: new Map(),
479
- currentAssetPaths: new Set(),
480
- currentAssetWatermarks: new Map(),
481
- currentDocumentReviewSessionIds: new Set(),
482
- currentDocumentReviewSessionWatermarks: new Map(),
483
- currentDocumentReviewCommentIds: new Set(),
484
- currentDocumentReviewCommentWatermarks: new Map(),
485
- currentAnnotatedAttachmentSessionIds: new Set(),
486
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
487
- currentTaskEventWatermarks: new Map(),
488
- deleteTaskIds: new Set(),
489
- pushedWatermarks: new Map([['task-1', '2026-03-14T12:00:00.000Z']]),
490
- emittedEventIds: ['evt-self-1']
491
- });
492
- runWorkspacePullSyncServiceMock.mockResolvedValue({
493
- success: true,
494
- syncedAt: '2026-03-15T18:05:00.000Z',
495
- cursor: null,
496
- pages: 1,
497
- appliedChanges: 0,
498
- pullRequestMs: 1,
499
- applyMs: 0,
500
- pulledDeleteTaskIds: new Set(),
501
- pulledActiveUpserts: false,
502
- pulledArchivedUpserts: false,
503
- documentDecryptFailures: []
504
- });
505
- const task = {
506
- id: 'task-1',
507
- status: 'task',
508
- title: 'Realtime suppression',
509
- createdAt: '2026-03-14T12:00:00.000Z',
510
- updatedAt: '2026-03-14T12:00:00.000Z'
511
- };
512
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
513
- initialProps: buildInput({
514
- realtimeSyncEnabled: true,
515
- tasks: [task]
516
- })
517
- });
518
- act(() => {
519
- result.current.applyWorkspaceSyncStateSnapshot({
520
- enabled: true,
521
- phase: 'active',
522
- pullCursor: null
523
- });
524
- });
525
- await act(async () => {
526
- await Promise.resolve();
527
- await Promise.resolve();
528
- await vi.advanceTimersByTimeAsync(400);
529
- });
530
- expect(MockWebSocket.instances).toHaveLength(1);
531
- const stabilizedPullCalls = runWorkspacePullSyncServiceMock.mock.calls.length;
532
- await act(async () => {
533
- await result.current.pushWorkspaceChangesToCloud('sig-self-event');
534
- await vi.advanceTimersByTimeAsync(200);
535
- });
536
- const pullCallsAfterPush = runWorkspacePullSyncServiceMock.mock.calls.length;
537
- expect(pullCallsAfterPush).toBeGreaterThanOrEqual(stabilizedPullCalls);
538
- act(() => {
539
- MockWebSocket.instances[0]?.open();
540
- MockWebSocket.instances[0]?.message({
541
- type: 'taskforce:update',
542
- workspaceId: 'workspace-local-active',
543
- eventId: 'evt-self-1'
544
- });
545
- });
546
- await act(async () => {
547
- await vi.advanceTimersByTimeAsync(300);
548
- });
549
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(pullCallsAfterPush);
550
- act(() => {
551
- MockWebSocket.instances[0]?.message({
552
- type: 'taskforce:update',
553
- workspaceId: 'workspace-local-active',
554
- eventId: 'evt-replay-2'
555
- });
556
- });
557
- await act(async () => {
558
- await vi.advanceTimersByTimeAsync(300);
559
- });
560
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(pullCallsAfterPush + 1);
561
- await act(async () => {
562
- unmount();
563
- await vi.runOnlyPendingTimersAsync();
564
- });
565
- });
566
- it('retries transient push failures automatically', async () => {
567
- runWorkspacePushSyncServiceMock
568
- .mockResolvedValueOnce({
569
- success: false,
570
- status: 503,
571
- transient: true,
572
- retryAfterMs: 200,
573
- requestMs: 1,
574
- changeCount: 1,
575
- deleteCount: 0,
576
- currentTaskIds: new Set(),
577
- deleteTaskIds: new Set(),
578
- currentDocumentPaths: new Set(),
579
- deleteDocumentPaths: new Set(),
580
- currentDocumentWatermarks: new Map(),
581
- pushedWatermarks: new Map(),
582
- emittedEventIds: []
583
- })
584
- .mockResolvedValueOnce({
585
- success: true,
586
- syncedAt: '2026-03-15T15:00:00.000Z',
587
- requestMs: 1,
588
- changeCount: 1,
589
- deleteCount: 0,
590
- currentTaskIds: new Set(['task-1']),
591
- deleteTaskIds: new Set(),
592
- currentDocumentPaths: new Set(),
593
- deleteDocumentPaths: new Set(),
594
- currentDocumentWatermarks: new Map(),
595
- pushedWatermarks: new Map([['task-1', '2026-03-14T12:00:00.000Z']]),
596
- emittedEventIds: []
597
- });
598
- const task = {
599
- id: 'task-1',
600
- status: 'task',
601
- title: 'Retry me',
602
- createdAt: '2026-03-14T12:00:00.000Z',
603
- updatedAt: '2026-03-14T12:00:00.000Z'
604
- };
605
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
606
- initialProps: buildInput({ tasks: [task] })
607
- });
608
- act(() => {
609
- result.current.applyWorkspaceSyncStateSnapshot({
610
- enabled: true,
611
- phase: 'provision-local',
612
- pullCursor: null
613
- });
614
- });
615
- expect(result.current.workspaceCloudSyncEnabled).toBe(true);
616
- expect(result.current.workspaceSyncPhase).toBe('provision-local');
617
- runWorkspacePushSyncServiceMock.mockClear();
618
- await act(async () => {
619
- await result.current.pushWorkspaceChangesToCloud('sig-push-retry');
620
- });
621
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
622
- expect(result.current.isWorkspaceRetryPending()).toBe(true);
623
- await act(async () => {
624
- await result.current.pushWorkspaceChangesToCloud('sig-push-retry');
625
- });
626
- await waitFor(() => {
627
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(2);
628
- });
629
- expect(result.current.workspaceSyncPhase).toBe('active');
630
- expect(result.current.workspaceLastPushAt).toBeTruthy();
631
- await act(async () => {
632
- await Promise.resolve();
633
- await Promise.resolve();
634
- });
635
- unmount();
636
- });
637
- it('keeps the last workspace error visible while a retry attempt is in flight', async () => {
638
- let resolveRetryPull = null;
639
- runWorkspacePullSyncServiceMock
640
- .mockResolvedValueOnce({
641
- success: false,
642
- kind: 'apply_all_candidates',
643
- status: 400,
644
- transient: true,
645
- hasTransientApplyFailure: true,
646
- error: 'Apply failed during bootstrap',
647
- failures: ['/api/taskforce/sync/workspace/apply-local:400'],
648
- cursor: null,
649
- pages: 0,
650
- appliedChanges: 0,
651
- pullRequestMs: 1,
652
- applyMs: 1,
653
- pulledDeleteTaskIds: new Set(),
654
- pulledActiveUpserts: false,
655
- pulledArchivedUpserts: false,
656
- documentDecryptFailures: []
657
- })
658
- .mockImplementationOnce(() => new Promise((resolve) => {
659
- resolveRetryPull = resolve;
660
- }))
661
- .mockResolvedValue({
662
- success: true,
663
- syncedAt: '2026-03-15T19:30:00.000Z',
664
- cursor: null,
665
- pages: 1,
666
- appliedChanges: 0,
667
- pullRequestMs: 1,
668
- applyMs: 0,
669
- pulledDeleteTaskIds: new Set(),
670
- pulledActiveUpserts: false,
671
- pulledArchivedUpserts: false,
672
- documentDecryptFailures: []
673
- });
674
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
675
- initialProps: buildInput()
676
- });
677
- act(() => {
678
- result.current.applyWorkspaceSyncStateSnapshot({
679
- enabled: true,
680
- phase: 'attach-cloud',
681
- pullCursor: null
682
- });
683
- });
684
- await waitFor(() => {
685
- expect(result.current.workspaceLastErrorMessage).toBe('Apply failed during bootstrap');
686
- expect(result.current.isWorkspaceRetryPending()).toBe(true);
687
- });
688
- act(() => {
689
- void result.current.retryWorkspaceCloudSync();
690
- });
691
- await waitFor(() => {
692
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(2);
693
- expect(result.current.workspaceSyncBusy).toBe(true);
694
- expect(result.current.workspaceLastErrorMessage).toBe('Apply failed during bootstrap');
695
- });
696
- await act(async () => {
697
- resolveRetryPull?.({
698
- success: true,
699
- syncedAt: '2026-03-15T19:30:00.000Z',
700
- cursor: null,
701
- pages: 1,
702
- appliedChanges: 0,
703
- pullRequestMs: 1,
704
- applyMs: 0,
705
- pulledDeleteTaskIds: new Set(),
706
- pulledActiveUpserts: false,
707
- pulledArchivedUpserts: false,
708
- documentDecryptFailures: []
709
- });
710
- });
711
- await waitFor(() => {
712
- expect(result.current.workspaceSyncBusy).toBe(false);
713
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T19:30:00.000Z');
714
- });
715
- unmount();
716
- });
717
- it('schedules a push when local task state changes without waiting for snapshot fingerprint changes', async () => {
718
- vi.useFakeTimers();
719
- runWorkspacePullSyncServiceMock.mockResolvedValue({
720
- success: true,
721
- syncedAt: '2026-03-15T19:30:00.000Z',
722
- cursor: null,
723
- pages: 1,
724
- appliedChanges: 0,
725
- pullRequestMs: 1,
726
- applyMs: 0,
727
- pulledDeleteTaskIds: new Set(),
728
- pulledActiveUpserts: false,
729
- pulledArchivedUpserts: false,
730
- documentDecryptFailures: []
731
- });
732
- runWorkspacePushSyncServiceMock.mockResolvedValue({
733
- success: true,
734
- syncedAt: '2026-03-15T15:00:00.000Z',
735
- requestMs: 1,
736
- changeCount: 1,
737
- deleteCount: 0,
738
- currentTaskIds: new Set(['task-local-1']),
739
- deleteTaskIds: new Set(),
740
- currentDocumentPaths: new Set(),
741
- deleteDocumentPaths: new Set(),
742
- currentDocumentWatermarks: new Map(),
743
- currentAssetPaths: new Set(),
744
- deleteAssetPaths: new Set(),
745
- currentAssetWatermarks: new Map(),
746
- currentDocumentReviewSessionIds: new Set(),
747
- deleteDocumentReviewSessionIds: new Set(),
748
- currentDocumentReviewSessionWatermarks: new Map(),
749
- currentAnnotatedAttachmentSessionIds: new Set(),
750
- deleteAnnotatedAttachmentSessionIds: new Set(),
751
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
752
- pushedWatermarks: new Map([['task-local-1', '2026-03-14T12:00:00.000Z']]),
753
- emittedEventIds: []
754
- });
755
- let input = buildInput();
756
- const { result, rerender, unmount } = renderHook((props) => useSyncOrchestrator(props), {
757
- initialProps: input
758
- });
759
- act(() => {
760
- result.current.applyWorkspaceSyncStateSnapshot({
761
- enabled: true,
762
- phase: 'provision-local',
763
- pullCursor: null
764
- });
765
- });
766
- await act(async () => {
767
- await vi.runOnlyPendingTimersAsync();
768
- await Promise.resolve();
769
- await Promise.resolve();
770
- });
771
- runWorkspacePushSyncServiceMock.mockClear();
772
- input = buildInput({
773
- tasks: [{
774
- id: 'task-local-1',
775
- title: 'Local pending task',
776
- status: 'task',
777
- createdAt: '2026-03-14T12:00:00.000Z',
778
- updatedAt: '2026-03-14T12:00:00.000Z',
779
- localReferenceNumber: 7,
780
- referenceNumber: null
781
- }]
782
- });
783
- rerender(input);
784
- await act(async () => {
785
- await vi.advanceTimersByTimeAsync(1600);
786
- await Promise.resolve();
787
- await Promise.resolve();
788
- });
789
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
790
- const pushedChanges = runWorkspacePushSyncServiceMock.mock.calls
791
- .map((call) => call?.[0]?.payload?.changes)
792
- .filter(Array.isArray);
793
- expect(pushedChanges).toEqual(expect.arrayContaining([
794
- expect.arrayContaining([
795
- expect.objectContaining({
796
- op: 'upsert',
797
- archived: false,
798
- task: expect.objectContaining({
799
- id: 'task-local-1'
800
- })
801
- })
802
- ])
803
- ]));
804
- unmount();
805
- });
806
- it('schedules a push when only task activity events change', async () => {
807
- vi.useFakeTimers();
808
- const baseTask = {
809
- id: 'task-local-activity-1',
810
- title: 'Local task activity',
811
- status: 'task',
812
- createdAt: '2026-03-14T12:00:00.000Z',
813
- updatedAt: '2026-03-14T12:00:00.000Z',
814
- taskEvents: []
815
- };
816
- runWorkspacePushSyncServiceMock.mockResolvedValue({
817
- success: true,
818
- syncedAt: '2026-03-15T15:05:00.000Z',
819
- requestMs: 1,
820
- changeCount: 1,
821
- deleteCount: 0,
822
- currentTaskIds: new Set(['task-local-activity-1']),
823
- deleteTaskIds: new Set(),
824
- currentDocumentPaths: new Set(),
825
- deleteDocumentPaths: new Set(),
826
- currentDocumentWatermarks: new Map(),
827
- currentAssetPaths: new Set(),
828
- deleteAssetPaths: new Set(),
829
- currentAssetWatermarks: new Map(),
830
- currentDocumentReviewSessionIds: new Set(),
831
- deleteDocumentReviewSessionIds: new Set(),
832
- currentDocumentReviewSessionWatermarks: new Map(),
833
- currentAnnotatedAttachmentSessionIds: new Set(),
834
- deleteAnnotatedAttachmentSessionIds: new Set(),
835
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
836
- currentTaskEventWatermarks: new Map(),
837
- pushedWatermarks: new Map([['task-local-activity-1', '2026-03-14T12:00:00.000Z']]),
838
- emittedEventIds: []
839
- });
840
- let input = buildInput({
841
- tasks: [baseTask]
842
- });
843
- const { result, rerender, unmount } = renderHook((props) => useSyncOrchestrator(props), {
844
- initialProps: input
845
- });
846
- act(() => {
847
- result.current.applyWorkspaceSyncStateSnapshot({
848
- enabled: true,
849
- phase: 'provision-local',
850
- pullCursor: null
851
- });
852
- });
853
- await act(async () => {
854
- await vi.runOnlyPendingTimersAsync();
855
- await Promise.resolve();
856
- await Promise.resolve();
857
- });
858
- runWorkspacePushSyncServiceMock.mockClear();
859
- input = buildInput({
860
- tasks: [{
861
- ...baseTask,
862
- taskEvents: [{
863
- id: 'event-1',
864
- taskId: 'task-local-activity-1',
865
- workspaceId: 'workspace-local-active',
866
- action: 'updated',
867
- actor: 'user-1',
868
- actorType: 'human',
869
- createdAt: '2026-03-15T15:06:00.000Z',
870
- details: {
871
- changes: {
872
- status: {
873
- from: 'task',
874
- to: 'task'
875
- }
876
- }
877
- }
878
- }]
879
- }]
880
- });
881
- rerender(input);
882
- await act(async () => {
883
- await vi.advanceTimersByTimeAsync(1600);
884
- await Promise.resolve();
885
- await Promise.resolve();
886
- });
887
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
888
- expect(runWorkspacePushSyncServiceMock.mock.calls[0]?.[0]?.payload?.changes).toEqual(expect.arrayContaining([
889
- expect.objectContaining({
890
- op: 'task-event-upsert',
891
- event: expect.objectContaining({
892
- id: 'event-1',
893
- taskId: 'task-local-activity-1'
894
- })
895
- })
896
- ]));
897
- unmount();
898
- });
899
- it('schedules a delete push when authoritative deleted task state changes', async () => {
900
- vi.useFakeTimers();
901
- const baseTask = {
902
- id: 'task-local-delete-1',
903
- title: 'Local delete task',
904
- status: 'task',
905
- createdAt: '2026-03-14T12:00:00.000Z',
906
- updatedAt: '2026-03-14T12:00:00.000Z'
907
- };
908
- runWorkspacePushSyncServiceMock.mockResolvedValue({
909
- success: true,
910
- syncedAt: '2026-03-15T15:05:00.000Z',
911
- requestMs: 1,
912
- changeCount: 1,
913
- deleteCount: 0,
914
- currentTaskIds: new Set(['task-local-delete-1']),
915
- deleteTaskIds: new Set(),
916
- currentDocumentPaths: new Set(),
917
- deleteDocumentPaths: new Set(),
918
- currentDocumentWatermarks: new Map(),
919
- currentAssetPaths: new Set(),
920
- deleteAssetPaths: new Set(),
921
- currentAssetWatermarks: new Map(),
922
- currentDocumentReviewSessionIds: new Set(),
923
- deleteDocumentReviewSessionIds: new Set(),
924
- currentDocumentReviewSessionWatermarks: new Map(),
925
- currentAnnotatedAttachmentSessionIds: new Set(),
926
- deleteAnnotatedAttachmentSessionIds: new Set(),
927
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
928
- pushedWatermarks: new Map([['task-local-delete-1', '2026-03-14T12:00:00.000Z']]),
929
- emittedEventIds: []
930
- });
931
- let input = buildInput({
932
- tasks: [baseTask]
933
- });
934
- const { result, rerender, unmount } = renderHook((props) => useSyncOrchestrator(props), {
935
- initialProps: input
936
- });
937
- act(() => {
938
- result.current.applyWorkspaceSyncStateSnapshot({
939
- enabled: true,
940
- phase: 'provision-local',
941
- pullCursor: null
942
- });
943
- });
944
- await act(async () => {
945
- await vi.runOnlyPendingTimersAsync();
946
- await Promise.resolve();
947
- await Promise.resolve();
948
- });
949
- runWorkspacePushSyncServiceMock.mockClear();
950
- runWorkspacePushSyncServiceMock.mockResolvedValue({
951
- success: true,
952
- syncedAt: '2026-03-15T15:06:00.000Z',
953
- requestMs: 1,
954
- changeCount: 0,
955
- deleteCount: 1,
956
- currentTaskIds: new Set(),
957
- deleteTaskIds: new Set(['task-local-delete-1']),
958
- currentDocumentPaths: new Set(),
959
- deleteDocumentPaths: new Set(),
960
- currentDocumentWatermarks: new Map(),
961
- currentAssetPaths: new Set(),
962
- deleteAssetPaths: new Set(),
963
- currentAssetWatermarks: new Map(),
964
- currentDocumentReviewSessionIds: new Set(),
965
- deleteDocumentReviewSessionIds: new Set(),
966
- currentDocumentReviewSessionWatermarks: new Map(),
967
- currentAnnotatedAttachmentSessionIds: new Set(),
968
- deleteAnnotatedAttachmentSessionIds: new Set(),
969
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
970
- pushedWatermarks: new Map(),
971
- emittedEventIds: []
972
- });
973
- input = buildInput({
974
- tasks: [],
975
- deletedTasks: [{
976
- id: 'deleted-task-local-delete-1',
977
- taskId: 'task-local-delete-1',
978
- workspaceId: 'workspace-local-active',
979
- deletedAt: '2026-03-15T15:06:00.000Z',
980
- actor: 'user-1',
981
- actorType: 'human',
982
- source: 'ui',
983
- taskSnapshot: baseTask
984
- }]
985
- });
986
- rerender(input);
987
- await act(async () => {
988
- await vi.advanceTimersByTimeAsync(1600);
989
- await Promise.resolve();
990
- await Promise.resolve();
991
- });
992
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
993
- const pushedChanges = runWorkspacePushSyncServiceMock.mock.calls
994
- .map((call) => call?.[0]?.payload?.changes)
995
- .filter(Array.isArray);
996
- expect(pushedChanges).toEqual(expect.arrayContaining([
997
- expect.arrayContaining([
998
- expect.objectContaining({
999
- op: 'delete',
1000
- taskId: 'task-local-delete-1',
1001
- deletedAt: '2026-03-15T15:06:00.000Z'
1002
- })
1003
- ])
1004
- ]));
1005
- unmount();
1006
- });
1007
- it('runs a startup full reconcile after the first successful pull when a saved cursor exists', async () => {
1008
- vi.useFakeTimers();
1009
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1010
- success: true,
1011
- syncedAt: '2026-03-15T19:30:00.000Z',
1012
- cursor: 'server-cursor-1',
1013
- pages: 1,
1014
- appliedChanges: 0,
1015
- pullRequestMs: 1,
1016
- applyMs: 0,
1017
- pulledDeleteTaskIds: new Set(),
1018
- pulledActiveUpserts: false,
1019
- pulledArchivedUpserts: false,
1020
- documentDecryptFailures: []
1021
- });
1022
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1023
- initialProps: buildInput()
1024
- });
1025
- act(() => {
1026
- result.current.applyWorkspaceSyncStateSnapshot({
1027
- enabled: true,
1028
- phase: 'active',
1029
- pullCursor: 'stale-cursor'
1030
- });
1031
- });
1032
- await act(async () => {
1033
- await vi.advanceTimersByTimeAsync(300);
1034
- await Promise.resolve();
1035
- await Promise.resolve();
1036
- });
1037
- expect(runWorkspacePullSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(2);
1038
- expect(runWorkspacePullSyncServiceMock.mock.calls.some((call) => call?.[0]?.cursor === 'stale-cursor')).toBe(true);
1039
- expect(runWorkspacePullSyncServiceMock.mock.calls.some((call) => call?.[0]?.cursor === null)).toBe(true);
1040
- unmount();
1041
- });
1042
- it('clears an invalid saved pull cursor and retries from the beginning', async () => {
1043
- vi.useFakeTimers();
1044
- runWorkspacePullSyncServiceMock
1045
- .mockResolvedValueOnce({
1046
- success: false,
1047
- kind: 'pull_http',
1048
- status: 400,
1049
- errorCode: 'INVALID_SYNC_CURSOR',
1050
- error: 'Invalid sync cursor.',
1051
- cursor: 'stale-cursor',
1052
- pages: 0,
1053
- appliedChanges: 0,
1054
- pullRequestMs: 1,
1055
- applyMs: 0,
1056
- pulledDeleteTaskIds: new Set(),
1057
- pulledActiveUpserts: false,
1058
- pulledArchivedUpserts: false,
1059
- documentDecryptFailures: []
1060
- })
1061
- .mockResolvedValueOnce({
1062
- success: true,
1063
- syncedAt: '2026-03-15T19:31:00.000Z',
1064
- cursor: 'fresh-cursor-1',
1065
- pages: 1,
1066
- appliedChanges: 0,
1067
- pullRequestMs: 1,
1068
- applyMs: 0,
1069
- pulledDeleteTaskIds: new Set(),
1070
- pulledActiveUpserts: false,
1071
- pulledArchivedUpserts: false,
1072
- documentDecryptFailures: []
1073
- });
1074
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1075
- initialProps: buildInput()
1076
- });
1077
- act(() => {
1078
- result.current.applyWorkspaceSyncStateSnapshot({
1079
- enabled: true,
1080
- phase: 'active',
1081
- pullCursor: 'stale-cursor'
1082
- });
1083
- });
1084
- await act(async () => {
1085
- await vi.advanceTimersByTimeAsync(500);
1086
- await Promise.resolve();
1087
- await Promise.resolve();
1088
- });
1089
- await act(async () => {
1090
- await vi.runOnlyPendingTimersAsync();
1091
- await Promise.resolve();
1092
- await Promise.resolve();
1093
- });
1094
- expect(runWorkspacePullSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(2);
1095
- expect(runWorkspacePullSyncServiceMock.mock.calls[0]?.[0]?.cursor).toBe('stale-cursor');
1096
- expect(runWorkspacePullSyncServiceMock.mock.calls.some((call) => call?.[0]?.cursor === null)).toBe(true);
1097
- expect(result.current.workspaceRetryAt).toBeNull();
1098
- expect(result.current.workspaceLastErrorMessage).toBeNull();
1099
- expect(result.current.workspaceSyncPhase).toBe('active');
1100
- unmount();
1101
- });
1102
- it('finishes provision-local bootstrap by moving to active after the first push', async () => {
1103
- runWorkspacePushSyncServiceMock.mockResolvedValue({
1104
- success: true,
1105
- syncedAt: '2026-03-15T18:00:00.000Z',
1106
- requestMs: 1,
1107
- changeCount: 0,
1108
- deleteCount: 0,
1109
- currentTaskIds: new Set(),
1110
- deleteTaskIds: new Set(),
1111
- currentDocumentPaths: new Set(),
1112
- deleteDocumentPaths: new Set(),
1113
- currentDocumentWatermarks: new Map(),
1114
- pushedWatermarks: new Map(),
1115
- emittedEventIds: []
1116
- });
1117
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1118
- initialProps: buildInput()
1119
- });
1120
- act(() => {
1121
- result.current.applyWorkspaceSyncStateSnapshot({
1122
- enabled: true,
1123
- phase: 'provision-local',
1124
- pullCursor: null
1125
- });
1126
- });
1127
- await waitFor(() => {
1128
- expect(result.current.workspaceSyncPhase).toBe('provision-local');
1129
- });
1130
- await act(async () => {
1131
- await result.current.pushWorkspaceChangesToCloud('sig-bootstrap');
1132
- });
1133
- await waitFor(() => {
1134
- expect(result.current.workspaceSyncPhase).toBe('active');
1135
- expect(result.current.workspaceLastPushAt).toBe('2026-03-15T18:00:00.000Z');
1136
- });
1137
- await act(async () => {
1138
- await Promise.resolve();
1139
- await Promise.resolve();
1140
- });
1141
- unmount();
1142
- });
1143
- it('finishes attach-cloud bootstrap after the first successful pull, including zero-change pulls', async () => {
1144
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1145
- success: true,
1146
- syncedAt: '2026-03-15T19:00:00.000Z',
1147
- cursor: null,
1148
- pages: 1,
1149
- appliedChanges: 0,
1150
- pullRequestMs: 1,
1151
- applyMs: 0,
1152
- pulledDeleteTaskIds: new Set(),
1153
- pulledActiveUpserts: false,
1154
- pulledArchivedUpserts: false,
1155
- documentDecryptFailures: []
1156
- });
1157
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1158
- initialProps: buildInput()
1159
- });
1160
- act(() => {
1161
- result.current.applyWorkspaceSyncStateSnapshot({
1162
- enabled: true,
1163
- phase: 'attach-cloud',
1164
- pullCursor: null
1165
- });
1166
- });
1167
- await waitFor(() => {
1168
- expect(result.current.workspaceSyncPhase).toBe('active');
1169
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T19:00:00.000Z');
1170
- });
1171
- const persisted = localStorage.getItem('taskforce.syncWatermarks.v3.workspace-local-active');
1172
- expect(persisted).toBeTruthy();
1173
- expect(JSON.parse(String(persisted))).toEqual(expect.objectContaining({
1174
- v: 11,
1175
- taskIds: [],
1176
- taxonomyStateFingerprint: expect.any(String),
1177
- documentWatermarks: [],
1178
- assetWatermarks: [],
1179
- taskEventWatermarks: []
1180
- }));
1181
- unmount();
1182
- });
1183
- it('persists task and planning baselines when attach-cloud bootstrap completes', async () => {
1184
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1185
- success: true,
1186
- syncedAt: '2026-03-15T19:00:00.000Z',
1187
- cursor: null,
1188
- pages: 1,
1189
- appliedChanges: 0,
1190
- pullRequestMs: 1,
1191
- applyMs: 0,
1192
- pulledDeleteTaskIds: new Set(),
1193
- pulledActiveUpserts: false,
1194
- pulledArchivedUpserts: false,
1195
- documentDecryptFailures: []
1196
- });
1197
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1198
- initialProps: buildInput({
1199
- tasks: [{
1200
- id: 'task-1',
1201
- title: 'Seeded task',
1202
- status: 'task',
1203
- createdAt: '2026-03-15T18:00:00.000Z',
1204
- updatedAt: '2026-03-15T18:05:00.000Z'
1205
- }],
1206
- initiatives: [{
1207
- id: 'initiative-1',
1208
- title: 'Seeded initiative',
1209
- createdAt: '2026-03-15T17:00:00.000Z',
1210
- updatedAt: '2026-03-15T17:05:00.000Z',
1211
- isArchived: false
1212
- }],
1213
- workstreams: [{
1214
- id: 'workstream-1',
1215
- title: 'Seeded workstream',
1216
- initiativeId: 'initiative-1',
1217
- createdAt: '2026-03-15T17:10:00.000Z',
1218
- updatedAt: '2026-03-15T17:15:00.000Z',
1219
- isArchived: false
1220
- }]
1221
- })
1222
- });
1223
- act(() => {
1224
- result.current.applyWorkspaceSyncStateSnapshot({
1225
- enabled: true,
1226
- phase: 'attach-cloud',
1227
- pullCursor: null
1228
- });
1229
- });
1230
- await waitFor(() => {
1231
- expect(result.current.workspaceSyncPhase).toBe('active');
1232
- });
1233
- const persisted = JSON.parse(String(localStorage.getItem('taskforce.syncWatermarks.v3.workspace-local-active')));
1234
- expect(persisted.taskIds).toEqual(['task-1']);
1235
- expect(persisted.taskWatermarks).toEqual([['task-1', '2026-03-15T18:05:00.000Z']]);
1236
- expect(persisted.initiativeWatermarks).toEqual([['initiative-1', '2026-03-15T17:05:00.000Z']]);
1237
- expect(persisted.workstreamWatermarks).toEqual([['workstream-1', '2026-03-15T17:15:00.000Z']]);
1238
- unmount();
1239
- });
1240
- it('rehydrates push baselines from workspace sync state when local storage is empty', async () => {
1241
- vi.stubGlobal('fetch', vi.fn((url, init) => {
1242
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1243
- return Promise.resolve({
1244
- ok: true,
1245
- json: () => Promise.resolve({
1246
- success: true,
1247
- state: {
1248
- version: 2,
1249
- enabled: true,
1250
- phase: 'active',
1251
- pullCursor: null,
1252
- pushBaseline: {
1253
- taskIds: ['task-1'],
1254
- taskWatermarks: [['task-1', '2026-03-15T18:05:00.000Z']],
1255
- initiativeWatermarks: [['initiative-1', '2026-03-15T17:05:00.000Z']],
1256
- workstreamWatermarks: [['workstream-1', '2026-03-15T17:15:00.000Z']],
1257
- aiProfileWatermarks: [['ai-profile-codex', '2026-03-15T18:00:00.000Z']],
1258
- documentWatermarks: [],
1259
- assetWatermarks: [],
1260
- documentReviewSessionWatermarks: [],
1261
- documentReviewCommentWatermarks: [],
1262
- annotatedAttachmentSessionWatermarks: [],
1263
- taskEventWatermarks: [],
1264
- taxonomyStateFingerprint: 'null'
1265
- }
1266
- }
1267
- })
1268
- });
1269
- }
1270
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1271
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1272
- }
1273
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1274
- }));
1275
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1276
- initialProps: buildInput({
1277
- tasks: [{
1278
- id: 'task-1',
1279
- title: 'Seeded task',
1280
- status: 'task',
1281
- createdAt: '2026-03-15T18:00:00.000Z',
1282
- updatedAt: '2026-03-15T18:05:00.000Z'
1283
- }]
1284
- })
1285
- });
1286
- expect(localStorage.getItem('taskforce.syncWatermarks.v3.workspace-local-active')).toBeNull();
1287
- await act(async () => {
1288
- await result.current.loadWorkspaceSyncState();
1289
- });
1290
- await waitFor(() => {
1291
- const persisted = JSON.parse(String(localStorage.getItem('taskforce.syncWatermarks.v3.workspace-local-active')));
1292
- expect(persisted.taskIds).toEqual(['task-1']);
1293
- expect(persisted.taskWatermarks).toEqual([['task-1', '2026-03-15T18:05:00.000Z']]);
1294
- expect(persisted.initiativeWatermarks).toEqual([['initiative-1', '2026-03-15T17:05:00.000Z']]);
1295
- expect(persisted.workstreamWatermarks).toEqual([['workstream-1', '2026-03-15T17:15:00.000Z']]);
1296
- expect(persisted.aiProfileWatermarks).toEqual([['ai-profile-codex', '2026-03-15T18:00:00.000Z']]);
1297
- });
1298
- unmount();
1299
- });
1300
- it('rehydrates task delete baselines from persisted taskIds even when task watermarks are empty', async () => {
1301
- vi.useFakeTimers();
1302
- vi.stubGlobal('fetch', vi.fn((url, init) => {
1303
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1304
- return Promise.resolve({
1305
- ok: true,
1306
- json: () => Promise.resolve({
1307
- success: true,
1308
- state: {
1309
- version: 2,
1310
- enabled: true,
1311
- phase: 'active',
1312
- pullCursor: null,
1313
- pushBaseline: {
1314
- taskIds: ['task-delete-hydrated-1'],
1315
- taskWatermarks: [],
1316
- initiativeWatermarks: [],
1317
- workstreamWatermarks: [],
1318
- aiProfileWatermarks: [],
1319
- documentWatermarks: [],
1320
- assetWatermarks: [],
1321
- documentReviewSessionWatermarks: [],
1322
- documentReviewCommentWatermarks: [],
1323
- annotatedAttachmentSessionWatermarks: [],
1324
- taskEventWatermarks: [],
1325
- taxonomyStateFingerprint: 'fingerprint-a'
1326
- }
1327
- }
1328
- })
1329
- });
1330
- }
1331
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1332
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1333
- }
1334
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1335
- }));
1336
- runWorkspacePushSyncServiceMock.mockResolvedValue({
1337
- success: true,
1338
- syncedAt: '2026-03-16T12:00:00.000Z',
1339
- requestMs: 1,
1340
- changeCount: 0,
1341
- deleteCount: 1,
1342
- currentTaskIds: new Set(),
1343
- deleteTaskIds: new Set(['task-delete-hydrated-1']),
1344
- currentDocumentPaths: new Set(),
1345
- deleteDocumentPaths: new Set(),
1346
- currentDocumentWatermarks: new Map(),
1347
- currentAssetPaths: new Set(),
1348
- deleteAssetPaths: new Set(),
1349
- currentAssetWatermarks: new Map(),
1350
- currentDocumentReviewSessionIds: new Set(),
1351
- deleteDocumentReviewSessionIds: new Set(),
1352
- currentDocumentReviewSessionWatermarks: new Map(),
1353
- currentAnnotatedAttachmentSessionIds: new Set(),
1354
- deleteAnnotatedAttachmentSessionIds: new Set(),
1355
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
1356
- pushedWatermarks: new Map(),
1357
- emittedEventIds: []
1358
- });
1359
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1360
- initialProps: buildInput({
1361
- tasks: [],
1362
- deletedTasks: [{
1363
- id: 'deleted-task-delete-hydrated-1',
1364
- taskId: 'task-delete-hydrated-1',
1365
- workspaceId: 'workspace-local-active',
1366
- deletedAt: '2026-03-16T11:59:00.000Z',
1367
- actor: 'user-1',
1368
- actorType: 'human',
1369
- source: 'ui',
1370
- taskSnapshot: {
1371
- id: 'task-delete-hydrated-1',
1372
- title: 'Deleted before reload',
1373
- status: 'task',
1374
- createdAt: '2026-03-15T10:00:00.000Z',
1375
- updatedAt: '2026-03-15T10:00:00.000Z'
1376
- }
1377
- }]
1378
- })
1379
- });
1380
- await act(async () => {
1381
- await result.current.loadWorkspaceSyncState();
1382
- });
1383
- act(() => {
1384
- result.current.applyWorkspaceSyncStateSnapshot({
1385
- enabled: true,
1386
- phase: 'active',
1387
- pullCursor: null
1388
- });
1389
- });
1390
- await act(async () => {
1391
- await vi.advanceTimersByTimeAsync(1600);
1392
- await Promise.resolve();
1393
- await Promise.resolve();
1394
- });
1395
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
1396
- const pushedChanges = runWorkspacePushSyncServiceMock.mock.calls
1397
- .map((call) => call?.[0]?.payload?.changes)
1398
- .filter(Array.isArray);
1399
- expect(pushedChanges).toEqual(expect.arrayContaining([
1400
- expect.arrayContaining([
1401
- expect.objectContaining({
1402
- op: 'delete',
1403
- taskId: 'task-delete-hydrated-1',
1404
- deletedAt: '2026-03-16T11:59:00.000Z'
1405
- })
1406
- ])
1407
- ]));
1408
- unmount();
1409
- });
1410
- it('finishes attach-cloud bootstrap locally even if persisting the active phase fails', async () => {
1411
- vi.stubGlobal('fetch', vi.fn((url, init) => {
1412
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1413
- return Promise.resolve({
1414
- ok: true,
1415
- json: () => Promise.resolve({
1416
- success: true,
1417
- state: {
1418
- version: 2,
1419
- enabled: true,
1420
- phase: 'attach-cloud',
1421
- pullCursor: null,
1422
- lastPullAt: null,
1423
- lastPushAt: null,
1424
- lastSyncedAt: null
1425
- }
1426
- })
1427
- });
1428
- }
1429
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1430
- return Promise.resolve({
1431
- ok: false,
1432
- status: 502,
1433
- json: () => Promise.resolve({ success: false, error: 'Bad gateway' })
1434
- });
1435
- }
1436
- if (url.includes('/api/taskforce/sync/workspace/documents')) {
1437
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
1438
- }
1439
- if (url.includes('/api/taskforce/sync/workspace/annotated-attachments')) {
1440
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
1441
- }
1442
- if (url.includes('/api/taskforce/sync/events')) {
1443
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1444
- }
1445
- if (url.includes('/api/taskforce/tasks')) {
1446
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1447
- }
1448
- if (url.includes('/api/taskforce/archive')) {
1449
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1450
- }
1451
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1452
- }));
1453
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1454
- success: true,
1455
- syncedAt: '2026-03-15T19:05:00.000Z',
1456
- cursor: null,
1457
- pages: 1,
1458
- appliedChanges: 0,
1459
- pullRequestMs: 1,
1460
- applyMs: 0,
1461
- pulledDeleteTaskIds: new Set(),
1462
- pulledActiveUpserts: false,
1463
- pulledArchivedUpserts: false,
1464
- documentDecryptFailures: []
1465
- });
1466
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1467
- initialProps: buildInput()
1468
- });
1469
- act(() => {
1470
- result.current.applyWorkspaceSyncStateSnapshot({
1471
- enabled: true,
1472
- phase: 'attach-cloud',
1473
- pullCursor: null
1474
- });
1475
- });
1476
- await waitFor(() => {
1477
- expect(result.current.workspaceSyncPhase).toBe('active');
1478
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T19:05:00.000Z');
1479
- });
1480
- expect(result.current.workspaceLastErrorMessage).toBeNull();
1481
- expect(result.current.workspaceSyncStatus).toBe('healthy');
1482
- unmount();
1483
- });
1484
- it('does not auto-push local state immediately after attach-cloud bootstrap completes', async () => {
1485
- vi.useFakeTimers();
1486
- const task = {
1487
- id: 'task-1',
1488
- status: 'task',
1489
- title: 'Local stale task',
1490
- createdAt: '2026-03-14T12:00:00.000Z',
1491
- updatedAt: '2026-03-14T12:00:00.000Z'
1492
- };
1493
- vi.stubGlobal('fetch', vi.fn((url, init) => {
1494
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1495
- return Promise.resolve({
1496
- ok: true,
1497
- json: () => Promise.resolve({
1498
- success: true,
1499
- state: {
1500
- version: 2,
1501
- enabled: true,
1502
- phase: 'attach-cloud',
1503
- pullCursor: null,
1504
- lastPullAt: null,
1505
- lastPushAt: null,
1506
- lastSyncedAt: null
1507
- }
1508
- })
1509
- });
1510
- }
1511
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1512
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1513
- }
1514
- if (url.includes('/api/taskforce/sync/workspace/documents')) {
1515
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
1516
- }
1517
- if (url.includes('/api/taskforce/sync/workspace/annotated-attachments')) {
1518
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
1519
- }
1520
- if (url.includes('/api/taskforce/sync/events')) {
1521
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1522
- }
1523
- if (url.includes('/api/taskforce/tasks')) {
1524
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1525
- }
1526
- if (url.includes('/api/taskforce/archive')) {
1527
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1528
- }
1529
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1530
- }));
1531
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1532
- success: true,
1533
- syncedAt: '2026-03-15T19:00:00.000Z',
1534
- cursor: null,
1535
- pages: 1,
1536
- appliedChanges: 0,
1537
- pullRequestMs: 1,
1538
- applyMs: 0,
1539
- pulledDeleteTaskIds: new Set(),
1540
- pulledActiveUpserts: false,
1541
- pulledArchivedUpserts: false,
1542
- documentDecryptFailures: []
1543
- });
1544
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1545
- initialProps: buildInput({ tasks: [task] })
1546
- });
1547
- act(() => {
1548
- result.current.applyWorkspaceSyncStateSnapshot({
1549
- enabled: true,
1550
- phase: 'attach-cloud',
1551
- pullCursor: null
1552
- });
1553
- });
1554
- await act(async () => {
1555
- await vi.advanceTimersByTimeAsync(2000);
1556
- });
1557
- expect(result.current.workspaceSyncPhase).toBe('active');
1558
- expect(runWorkspacePushSyncServiceMock).not.toHaveBeenCalled();
1559
- unmount();
1560
- });
1561
- it('prevents duplicate local windows from running the same pull concurrently', async () => {
1562
- let resolveFirstPull = null;
1563
- runWorkspacePullSyncServiceMock.mockImplementationOnce(() => new Promise((resolve) => {
1564
- resolveFirstPull = resolve;
1565
- }));
1566
- const firstHook = renderHook((props) => useSyncOrchestrator(props), {
1567
- initialProps: buildInput()
1568
- });
1569
- const secondHook = renderHook((props) => useSyncOrchestrator(props), {
1570
- initialProps: buildInput()
1571
- });
1572
- act(() => {
1573
- firstHook.result.current.applyWorkspaceSyncStateSnapshot({
1574
- enabled: true,
1575
- phase: 'active',
1576
- pullCursor: null
1577
- });
1578
- });
1579
- await waitFor(() => {
1580
- expect(runWorkspacePullSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
1581
- expect(firstHook.result.current.workspaceSyncBusy).toBe(true);
1582
- });
1583
- const callsWhileFirstWindowOwnsLease = runWorkspacePullSyncServiceMock.mock.calls.length;
1584
- act(() => {
1585
- secondHook.result.current.applyWorkspaceSyncStateSnapshot({
1586
- enabled: true,
1587
- phase: 'active',
1588
- pullCursor: null
1589
- });
1590
- });
1591
- await waitFor(() => {
1592
- expect(secondHook.result.current.workspaceLastErrorMessage).toMatch(/another local window/i);
1593
- });
1594
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(callsWhileFirstWindowOwnsLease);
1595
- await act(async () => {
1596
- resolveFirstPull?.({
1597
- success: true,
1598
- syncedAt: '2026-03-15T20:00:00.000Z',
1599
- cursor: null,
1600
- pages: 1,
1601
- appliedChanges: 0,
1602
- pullRequestMs: 1,
1603
- applyMs: 0,
1604
- pulledDeleteTaskIds: new Set(),
1605
- pulledActiveUpserts: false,
1606
- pulledArchivedUpserts: false,
1607
- documentDecryptFailures: []
1608
- });
1609
- });
1610
- firstHook.unmount();
1611
- secondHook.unmount();
1612
- });
1613
- it('marks stalled bootstrap as error and keeps repair actionable', async () => {
1614
- vi.useFakeTimers();
1615
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1616
- initialProps: buildInput({
1617
- isAuthenticated: false,
1618
- checkAuthSession: vi.fn(async () => false)
1619
- })
1620
- });
1621
- act(() => {
1622
- result.current.applyWorkspaceSyncStateSnapshot({
1623
- enabled: true,
1624
- phase: 'attach-cloud',
1625
- pullCursor: null
1626
- });
1627
- });
1628
- await act(async () => {
1629
- await vi.advanceTimersByTimeAsync(130_000);
1630
- await Promise.resolve();
1631
- await Promise.resolve();
1632
- });
1633
- expect(result.current.workspaceSyncPhase).toBe('error');
1634
- expect(result.current.workspaceLastErrorMessage).toMatch(/stalled/i);
1635
- expect(result.current.workspaceSyncRecommendedAction).toMatch(/repair/i);
1636
- unmount();
1637
- });
1638
- it('does not mark bootstrap stalled while pull is slow but making progress', async () => {
1639
- vi.useFakeTimers();
1640
- runWorkspacePullSyncServiceMock.mockImplementation(async (input) => {
1641
- input.onPagePulled?.();
1642
- await new Promise((resolve) => {
1643
- setTimeout(resolve, 70_000);
1644
- });
1645
- input.onChangesApplied?.(200);
1646
- await new Promise((resolve) => {
1647
- setTimeout(resolve, 70_000);
1648
- });
1649
- return {
1650
- success: true,
1651
- syncedAt: '2026-03-15T20:30:00.000Z',
1652
- cursor: null,
1653
- pages: 2,
1654
- appliedChanges: 200,
1655
- pullRequestMs: 140_000,
1656
- applyMs: 100,
1657
- pulledDeleteTaskIds: new Set(),
1658
- pulledActiveUpserts: true,
1659
- pulledArchivedUpserts: false,
1660
- documentDecryptFailures: []
1661
- };
1662
- });
1663
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1664
- initialProps: buildInput()
1665
- });
1666
- act(() => {
1667
- result.current.applyWorkspaceSyncStateSnapshot({
1668
- enabled: true,
1669
- phase: 'attach-cloud',
1670
- pullCursor: null
1671
- });
1672
- });
1673
- await act(async () => {
1674
- await vi.advanceTimersByTimeAsync(130_000);
1675
- await Promise.resolve();
1676
- await Promise.resolve();
1677
- });
1678
- expect(result.current.workspaceSyncPhase).toBe('attach-cloud');
1679
- expect(result.current.workspaceLastErrorMessage).toBeNull();
1680
- await act(async () => {
1681
- await vi.advanceTimersByTimeAsync(20_000);
1682
- await Promise.resolve();
1683
- await Promise.resolve();
1684
- });
1685
- expect(result.current.workspaceSyncPhase).toBe('active');
1686
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T20:30:00.000Z');
1687
- unmount();
1688
- });
1689
- it('does not mark bootstrap stalled while the first pull page is still in flight without progress callbacks yet', async () => {
1690
- vi.useFakeTimers();
1691
- let resolvePull = null;
1692
- runWorkspacePullSyncServiceMock.mockImplementation(() => new Promise((resolve) => {
1693
- resolvePull = resolve;
1694
- }));
1695
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1696
- initialProps: buildInput()
1697
- });
1698
- act(() => {
1699
- result.current.applyWorkspaceSyncStateSnapshot({
1700
- enabled: true,
1701
- phase: 'attach-cloud',
1702
- pullCursor: null
1703
- });
1704
- });
1705
- await act(async () => {
1706
- await vi.advanceTimersByTimeAsync(130_000);
1707
- await Promise.resolve();
1708
- await Promise.resolve();
1709
- });
1710
- expect(result.current.workspaceSyncPhase).toBe('attach-cloud');
1711
- expect(result.current.workspaceLastErrorMessage).toBeNull();
1712
- await act(async () => {
1713
- resolvePull?.({
1714
- success: true,
1715
- syncedAt: '2026-03-15T20:45:00.000Z',
1716
- cursor: null,
1717
- pages: 1,
1718
- appliedChanges: 0,
1719
- pullRequestMs: 130_000,
1720
- applyMs: 0,
1721
- pulledDeleteTaskIds: new Set(),
1722
- pulledActiveUpserts: false,
1723
- pulledArchivedUpserts: false,
1724
- documentDecryptFailures: []
1725
- });
1726
- await Promise.resolve();
1727
- await Promise.resolve();
1728
- });
1729
- expect(result.current.workspaceSyncPhase).toBe('active');
1730
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T20:45:00.000Z');
1731
- unmount();
1732
- });
1733
- it('repair resets bootstrap state and retries attach-cloud deterministically', async () => {
1734
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1735
- success: true,
1736
- syncedAt: '2026-03-15T21:00:00.000Z',
1737
- cursor: null,
1738
- pages: 1,
1739
- appliedChanges: 0,
1740
- pullRequestMs: 1,
1741
- applyMs: 0,
1742
- pulledDeleteTaskIds: new Set(),
1743
- pulledActiveUpserts: false,
1744
- pulledArchivedUpserts: false,
1745
- documentDecryptFailures: []
1746
- });
1747
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1748
- initialProps: buildInput()
1749
- });
1750
- act(() => {
1751
- result.current.applyWorkspaceSyncStateSnapshot({
1752
- enabled: true,
1753
- phase: 'attach-cloud',
1754
- pullCursor: 'stale-cursor'
1755
- });
1756
- result.current.applyWorkspaceSyncStateSnapshot({
1757
- enabled: true,
1758
- phase: 'error',
1759
- pullCursor: 'stale-cursor',
1760
- lastErrorMessage: 'Workspace sync initial cloud pull stalled. Press Repair to restart sync for this workspace.'
1761
- });
1762
- });
1763
- await act(async () => {
1764
- await result.current.resetWorkspaceSyncCursorAndPull();
1765
- });
1766
- expect(runWorkspacePullSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
1767
- expect(runWorkspacePushSyncServiceMock).not.toHaveBeenCalled();
1768
- expect(result.current.workspacePullCursorRef.current).toBeNull();
1769
- expect(result.current.workspaceSyncPhase).toBe('active');
1770
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T21:00:00.000Z');
1771
- unmount();
1772
- });
1773
- it('blocks same-window push and pull reentry while repair is active', async () => {
1774
- let resolveRepairPull = null;
1775
- runWorkspacePullSyncServiceMock.mockImplementationOnce(() => new Promise((resolve) => {
1776
- resolveRepairPull = resolve;
1777
- }));
1778
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1779
- initialProps: buildInput()
1780
- });
1781
- act(() => {
1782
- result.current.applyWorkspaceSyncStateSnapshot({
1783
- enabled: true,
1784
- phase: 'attach-cloud',
1785
- pullCursor: 'stale-cursor'
1786
- });
1787
- result.current.applyWorkspaceSyncStateSnapshot({
1788
- enabled: true,
1789
- phase: 'error',
1790
- pullCursor: 'stale-cursor',
1791
- lastErrorMessage: 'Workspace sync initial cloud pull stalled. Press Repair to restart sync for this workspace.'
1792
- });
1793
- });
1794
- let repairPromise = null;
1795
- await act(async () => {
1796
- repairPromise = result.current.resetWorkspaceSyncCursorAndPull();
1797
- await Promise.resolve();
1798
- await Promise.resolve();
1799
- });
1800
- const pullCallsDuringRepair = runWorkspacePullSyncServiceMock.mock.calls.length;
1801
- expect(pullCallsDuringRepair).toBeGreaterThanOrEqual(1);
1802
- await act(async () => {
1803
- const pushed = await result.current.pushWorkspaceChangesToCloud('sig-during-repair');
1804
- const pulled = await result.current.pullWorkspaceChangesFromCloud();
1805
- expect(pushed).toBe(false);
1806
- expect(pulled).toBe(false);
1807
- });
1808
- expect(runWorkspacePushSyncServiceMock).not.toHaveBeenCalled();
1809
- expect(runWorkspacePullSyncServiceMock).toHaveBeenCalledTimes(pullCallsDuringRepair);
1810
- await act(async () => {
1811
- resolveRepairPull?.({
1812
- success: true,
1813
- syncedAt: '2026-03-15T21:05:00.000Z',
1814
- cursor: null,
1815
- pages: 1,
1816
- appliedChanges: 0,
1817
- pullRequestMs: 1,
1818
- applyMs: 0,
1819
- pulledDeleteTaskIds: new Set(),
1820
- pulledActiveUpserts: false,
1821
- pulledArchivedUpserts: false,
1822
- documentDecryptFailures: []
1823
- });
1824
- await repairPromise;
1825
- await Promise.resolve();
1826
- await Promise.resolve();
1827
- });
1828
- expect(result.current.workspaceSyncPhase).toBe('active');
1829
- expect(result.current.workspaceLastPullAt).toBe('2026-03-15T21:05:00.000Z');
1830
- unmount();
1831
- });
1832
- it('periodically retries a stale local push signature even without a new state change', async () => {
1833
- vi.useFakeTimers();
1834
- const localTask = {
1835
- id: 'task-local-watchdog',
1836
- title: 'Watchdog push recovery',
1837
- status: 'task',
1838
- priority: 2,
1839
- complexity: 3,
1840
- type: 'feature',
1841
- category: 'general',
1842
- createdAt: '2026-03-15T18:00:00.000Z',
1843
- updatedAt: '2026-03-15T18:00:00.000Z',
1844
- comments: [],
1845
- attachments: [],
1846
- checklistItems: [],
1847
- taxonomies: {},
1848
- assignee: 'unassigned',
1849
- referenceNumber: null,
1850
- localReferenceNumber: 18
1851
- };
1852
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1853
- initialProps: buildInput({ tasks: [localTask] })
1854
- });
1855
- act(() => {
1856
- result.current.applyWorkspaceSyncStateSnapshot({
1857
- enabled: true,
1858
- phase: 'active',
1859
- pullCursor: null
1860
- });
1861
- });
1862
- await act(async () => {
1863
- await result.current.pushWorkspaceChangesToCloud('sig-initial-watchdog');
1864
- });
1865
- expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledTimes(1);
1866
- runWorkspacePushSyncServiceMock.mockClear();
1867
- act(() => {
1868
- result.current.workspaceLastPushedSignatureRef.current = 'stale-signature';
1869
- result.current.workspacePullInFlightRef.current = true;
1870
- });
1871
- await act(async () => {
1872
- await vi.advanceTimersByTimeAsync(20_000);
1873
- });
1874
- expect(runWorkspacePushSyncServiceMock).not.toHaveBeenCalled();
1875
- act(() => {
1876
- result.current.workspacePullInFlightRef.current = false;
1877
- });
1878
- await act(async () => {
1879
- await vi.advanceTimersByTimeAsync(12_000);
1880
- });
1881
- expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledTimes(1);
1882
- expect(runWorkspacePushSyncServiceMock).toHaveBeenCalledWith(expect.objectContaining({
1883
- payload: expect.objectContaining({
1884
- changes: expect.arrayContaining([
1885
- expect.objectContaining({
1886
- op: 'upsert',
1887
- task: expect.objectContaining({
1888
- id: 'task-local-watchdog'
1889
- })
1890
- })
1891
- ])
1892
- })
1893
- }));
1894
- unmount();
1895
- });
1896
- it('forces a full ai-profile push during manual retry even when the workspace signature is unchanged', async () => {
1897
- vi.stubGlobal('fetch', vi.fn((url, init) => {
1898
- if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1899
- return Promise.resolve({
1900
- ok: true,
1901
- json: () => Promise.resolve({
1902
- success: true,
1903
- state: {
1904
- version: 2,
1905
- enabled: true,
1906
- phase: 'active',
1907
- pullCursor: null,
1908
- lastPullAt: null,
1909
- lastPushAt: null,
1910
- lastSyncedAt: null
1911
- }
1912
- })
1913
- });
1914
- }
1915
- if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
1916
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1917
- }
1918
- if (url.includes('/api/taskforce/sync/workspace/ai-profiles')) {
1919
- return Promise.resolve({
1920
- ok: true,
1921
- json: () => Promise.resolve({
1922
- aiProfiles: [{
1923
- id: 'ai-profile-codex',
1924
- workspaceId: 'workspace-local-active',
1925
- profileToken: 'token-codex',
1926
- name: 'Codex',
1927
- username: 'codex',
1928
- icon: 'Bot',
1929
- color: '#0ea5e9',
1930
- createdAt: '2026-03-15T18:00:00.000Z',
1931
- updatedAt: '2026-03-15T18:00:00.000Z',
1932
- lastActiveAt: '2026-03-15T18:00:00.000Z'
1933
- }]
1934
- })
1935
- });
1936
- }
1937
- if (url.includes('/api/taskforce/sync/workspace/documents')) {
1938
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
1939
- }
1940
- if (url.includes('/api/taskforce/sync/workspace/annotated-attachments')) {
1941
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
1942
- }
1943
- if (url.includes('/api/taskforce/sync/workspace/assets')) {
1944
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ assets: [] }) });
1945
- }
1946
- if (url.includes('/api/taskforce/sync/events')) {
1947
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1948
- }
1949
- if (url.includes('/api/taskforce/tasks')) {
1950
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
1951
- }
1952
- if (url.includes('/api/taskforce/archive')) {
1953
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
1954
- }
1955
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
1956
- }));
1957
- runWorkspacePullSyncServiceMock.mockResolvedValue({
1958
- success: true,
1959
- syncedAt: '2026-03-15T22:00:00.000Z',
1960
- cursor: null,
1961
- pages: 1,
1962
- appliedChanges: 0,
1963
- pullRequestMs: 1,
1964
- applyMs: 0,
1965
- pulledDeleteTaskIds: new Set(),
1966
- pulledActiveUpserts: false,
1967
- pulledArchivedUpserts: false,
1968
- documentDecryptFailures: []
1969
- });
1970
- runWorkspacePushSyncServiceMock.mockResolvedValue({
1971
- success: true,
1972
- syncedAt: '2026-03-15T22:00:01.000Z',
1973
- requestMs: 1,
1974
- changeCount: 1,
1975
- deleteCount: 0,
1976
- currentTaskIds: new Set(),
1977
- deleteTaskIds: new Set(),
1978
- currentAiProfileIds: new Set(['ai-profile-codex']),
1979
- currentAiProfileWatermarks: new Map([['ai-profile-codex', '2026-03-15T18:00:00.000Z']]),
1980
- currentDocumentPaths: new Set(),
1981
- deleteDocumentPaths: new Set(),
1982
- currentDocumentWatermarks: new Map(),
1983
- currentAssetPaths: new Set(),
1984
- deleteAssetPaths: new Set(),
1985
- currentAssetWatermarks: new Map(),
1986
- currentDocumentReviewSessionIds: new Set(),
1987
- deleteDocumentReviewSessionIds: new Set(),
1988
- currentDocumentReviewSessionWatermarks: new Map(),
1989
- currentAnnotatedAttachmentSessionIds: new Set(),
1990
- deleteAnnotatedAttachmentSessionIds: new Set(),
1991
- currentAnnotatedAttachmentSessionWatermarks: new Map(),
1992
- pushedWatermarks: new Map(),
1993
- emittedEventIds: []
1994
- });
1995
- const { result, unmount } = renderHook((props) => useSyncOrchestrator(props), {
1996
- initialProps: buildInput()
1997
- });
1998
- runWorkspacePushSyncServiceMock.mockClear();
1999
- await act(async () => {
2000
- await result.current.retryWorkspaceCloudSync();
2001
- });
2002
- await waitFor(() => {
2003
- expect(runWorkspacePushSyncServiceMock.mock.calls.length).toBeGreaterThanOrEqual(1);
2004
- });
2005
- const retryPayloads = runWorkspacePushSyncServiceMock.mock.calls
2006
- .map((call) => call?.[0]?.payload)
2007
- .filter(Boolean);
2008
- expect(retryPayloads).toEqual(expect.arrayContaining([
2009
- expect.objectContaining({
2010
- changes: expect.arrayContaining([
2011
- expect.objectContaining({
2012
- op: 'ai-profile-upsert',
2013
- profile: expect.objectContaining({
2014
- id: 'ai-profile-codex',
2015
- workspaceId: 'workspace-local-active',
2016
- name: 'Codex'
2017
- })
2018
- })
2019
- ])
2020
- })
2021
- ]));
2022
- const persisted = JSON.parse(String(localStorage.getItem('taskforce.syncWatermarks.v3.workspace-local-active')));
2023
- expect(persisted.aiProfileWatermarks).toEqual([['ai-profile-codex', '2026-03-15T18:00:00.000Z']]);
2024
- expect(result.current.workspaceSyncPendingChanges).toBe(0);
2025
- unmount();
2026
- });
2027
- });