@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,2932 +0,0 @@
1
- import { describe, it, expect, afterEach, vi } from 'vitest';
2
- import { Readable } from 'stream';
3
- import * as fs from 'fs';
4
- import * as os from 'os';
5
- import * as path from 'path';
6
- import { createHash } from 'crypto';
7
- import { TaskforceCore } from '../../core/Taskforce';
8
- import { createRoutes, matchRoute } from '../routes';
9
- import { createSessionTokenCookie } from '../auth';
10
- import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore';
11
- function createMockReq(method, url, body, headers) {
12
- const req = new Readable();
13
- req._read = () => { };
14
- req.push(body ? JSON.stringify(body) : null);
15
- req.push(null);
16
- req.method = method;
17
- req.url = url;
18
- req.headers = headers || {};
19
- return req;
20
- }
21
- function createMockRes() {
22
- return {
23
- writeHead: vi.fn(),
24
- end: vi.fn()
25
- };
26
- }
27
- describe('Server Routes Workspace Sync Integration', () => {
28
- const tempDirs = [];
29
- const originalHome = process.env.HOME;
30
- const workspaceId = 'workspace-local-active';
31
- const authConfig = {
32
- runtimeMode: 'cloud',
33
- enabled: true,
34
- requiredForApi: true,
35
- excludedPaths: ['/api/taskforce/health', '/api/taskforce/version', '/api/taskforce/auth'],
36
- defaultRole: 'member',
37
- defaultWorkspaceId: '',
38
- sessionCookieName: 'taskforce_session',
39
- sessionSecret: 'integration-test-secret',
40
- sessionTtlSeconds: 60 * 60,
41
- sessionCookieSameSite: 'None',
42
- sessionCookieSecure: true
43
- };
44
- const createAuthCookie = (overrideWorkspaceId) => createSessionTokenCookie('session-sync-integration', authConfig, {
45
- role: 'owner',
46
- workspaceId: overrideWorkspaceId || workspaceId,
47
- userId: 'integration-user'
48
- });
49
- const readJsonBody = (res) => JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
50
- const createIsolatedProjectRoot = (prefix) => {
51
- const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
52
- tempDirs.push(projectRoot);
53
- process.env.HOME = projectRoot;
54
- return projectRoot;
55
- };
56
- const buildSoftwareTaxonomyState = () => ({
57
- categories: [
58
- { value: 'default', label: 'General', icon: 'Inbox' },
59
- { value: 'product', label: 'Product', icon: 'Layers', color: 'blue-500' }
60
- ],
61
- types: [
62
- { value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500', status: 'active' },
63
- { value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500', status: 'active' },
64
- { value: 'chore', label: 'Chore', icon: 'Zap', color: 'amber-500', status: 'active' },
65
- { value: 'refactor', label: 'Refactor', icon: 'Code', color: 'blue-500', status: 'active' },
66
- { value: 'documentation', label: 'Docs', icon: 'Book', color: 'sky-500', status: 'active' }
67
- ],
68
- priorities: [
69
- { value: 1, label: 'Low', color: 'amber-200', icon: 'ArrowDown' },
70
- { value: 2, label: 'Medium', color: 'yellow-500', icon: 'Minus' },
71
- { value: 3, label: 'High', color: 'orange-500', icon: 'ArrowUp' },
72
- { value: 4, label: 'Critical', color: 'red-500', icon: 'AlertTriangle' }
73
- ],
74
- taxonomies: []
75
- });
76
- const seedAuthenticatedWorkspace = (core, options) => {
77
- const userId = String(options?.userId || 'integration-user');
78
- const targetWorkspaceId = String(options?.workspaceId || workspaceId);
79
- const now = new Date().toISOString();
80
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
81
- core.db.prepare(`
82
- INSERT OR IGNORE INTO billing_price_mappings (
83
- tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
84
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
- `).run('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
86
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
87
- core.db.prepare('INSERT OR IGNORE INTO users (id, tenant_id, email, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(userId, 'default', `${userId}@test.com`, now, now);
88
- core.updateAccountEntitlement({
89
- accountId: userId,
90
- planVersionId: 'starter-v1',
91
- actorUserId: 'system',
92
- actorRole: 'system_admin'
93
- });
94
- core.createWorkspaceForUser({
95
- userId,
96
- name: options?.name || 'Local Active Workspace',
97
- role: 'owner',
98
- preferredWorkspaceId: targetWorkspaceId
99
- });
100
- };
101
- afterEach(() => {
102
- process.env.HOME = originalHome;
103
- while (tempDirs.length > 0) {
104
- const dir = tempDirs.pop();
105
- if (!dir)
106
- continue;
107
- fs.rmSync(dir, { recursive: true, force: true });
108
- }
109
- });
110
- it('persists cloud-pulled tasks into local sqlite via apply-local route', async () => {
111
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-');
112
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
113
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
114
- const basePath = core.getPaths().basePath;
115
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
116
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
117
- expect(match).toBeTruthy();
118
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
119
- changes: [
120
- {
121
- op: 'upsert',
122
- archived: false,
123
- task: {
124
- id: 'task-cloud-1',
125
- title: 'Task from cloud',
126
- status: 'in-progress',
127
- canceledReason: null
128
- }
129
- }
130
- ]
131
- }, { 'x-taskforce-workspace-id': workspaceId });
132
- const res = createMockRes();
133
- await match?.handler(req, res, {});
134
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
135
- // Reload from disk to assert persistence, not just in-memory state.
136
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
137
- const persisted = reloadedCore.listTasks(workspaceId);
138
- const task = persisted.tasks.find((item) => item.id === 'task-cloud-1');
139
- const defaultWorkspaceTasks = reloadedCore.listTasks('default').tasks;
140
- expect(task).toBeTruthy();
141
- expect(task?.title).toBe('Task from cloud');
142
- expect(task?.status).toBe('in-progress');
143
- expect(defaultWorkspaceTasks.find((item) => item.id === 'task-cloud-1')).toBeUndefined();
144
- });
145
- it('lets the cloud task keep a colliding reference number during apply-local and records a diagnostic', async () => {
146
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-task-ref-collision-');
147
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
148
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
149
- const localTask = core.createTask({ title: 'Local incumbent' }, { workspaceId });
150
- expect(localTask.referenceLabel).toBe('T-1');
151
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
152
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
153
- expect(match).toBeTruthy();
154
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
155
- changes: [
156
- {
157
- op: 'upsert',
158
- archived: false,
159
- task: {
160
- id: 'task-cloud-ref-1',
161
- title: 'Cloud winner',
162
- status: 'task',
163
- referenceNumber: 1,
164
- updatedAt: '2026-04-10T16:00:00.000Z'
165
- }
166
- }
167
- ]
168
- }, { 'x-taskforce-workspace-id': workspaceId });
169
- const res = createMockRes();
170
- await match?.handler(req, res, {});
171
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
172
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
173
- expect(reloadedCore.getTask('task-cloud-ref-1', workspaceId)?.referenceLabel).toBe('T-1');
174
- expect(reloadedCore.getTask(localTask.id, workspaceId)?.referenceLabel).toBe('T-2');
175
- expect(reloadedCore.listSyncEvents(workspaceId, 10)).toEqual(expect.arrayContaining([
176
- expect.objectContaining({
177
- eventType: 'apply',
178
- status: 'error',
179
- errorMessage: 'Detected task reference number mismatch during normal sync.',
180
- details: expect.objectContaining({
181
- referenceLabel: 'T-1',
182
- existingTaskId: localTask.id,
183
- taskId: 'task-cloud-ref-1'
184
- })
185
- })
186
- ]));
187
- });
188
- it('persists synced comments via apply-local route', async () => {
189
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-comments-');
190
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
191
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
192
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
193
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
194
- expect(match).toBeTruthy();
195
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
196
- changes: [
197
- {
198
- op: 'upsert',
199
- archived: false,
200
- task: {
201
- id: 'task-cloud-comments-1',
202
- title: 'Task with synced comments',
203
- status: 'in-progress',
204
- updatedAt: '2026-03-16T12:00:00.000Z',
205
- comments: [
206
- {
207
- id: 'comment-cloud-1',
208
- author: 'user',
209
- text: 'Cloud comment',
210
- timestamp: '2026-03-16T12:00:00.000Z'
211
- }
212
- ]
213
- }
214
- }
215
- ]
216
- }, { 'x-taskforce-workspace-id': workspaceId });
217
- const res = createMockRes();
218
- await match?.handler(req, res, {});
219
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
220
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
221
- const persisted = reloadedCore.getTask('task-cloud-comments-1', workspaceId);
222
- expect(persisted?.comments).toEqual([
223
- expect.objectContaining({
224
- id: 'comment-cloud-1',
225
- author: 'user',
226
- text: 'Cloud comment',
227
- timestamp: '2026-03-16T12:00:00.000Z'
228
- })
229
- ]);
230
- });
231
- it('persists unattached annotated attachment sessions via apply-local route', async () => {
232
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-annotated-unattached-');
233
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
234
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
235
- const assetStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
236
- assetStore.upsertAsset({
237
- assetId: 'asset-image-sync-1',
238
- workspaceId,
239
- kind: 'image',
240
- originalFilename: 'synced-image.png',
241
- mimeType: 'image/png',
242
- storageProvider: 'local',
243
- storageKey: 'workspaces/workspace-local-active/assets/images/asset-image-sync-1-synced-image.png',
244
- contentSha256: 'seed-hash-sync-image',
245
- sizeBytes: 42,
246
- createdAt: '2026-03-30T20:00:00.000Z',
247
- updatedAt: '2026-03-30T20:00:00.000Z'
248
- });
249
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
250
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
251
- expect(match).toBeTruthy();
252
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
253
- changes: [
254
- {
255
- op: 'annotated-attachment-session-upsert',
256
- session: {
257
- id: 'session-unattached-apply-1',
258
- workspaceId,
259
- taskId: '',
260
- baseImageAssetId: 'asset-image-sync-1',
261
- title: 'Synced loose notes',
262
- globalInstruction: 'Review this image',
263
- annotations: [
264
- { id: 'annotation-1', kind: 'pin', order: 0, instruction: 'Look here', x: 0.2, y: 0.3 }
265
- ],
266
- createdByActorId: null,
267
- updatedByActorId: null,
268
- createdAt: '2026-03-30T20:00:00.000Z',
269
- updatedAt: '2026-03-30T20:05:00.000Z',
270
- deletedAt: null
271
- }
272
- }
273
- ]
274
- }, { 'x-taskforce-workspace-id': workspaceId });
275
- const res = createMockRes();
276
- await match?.handler(req, res, {});
277
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
278
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
279
- expect(reloadedCore.listAnnotatedAttachmentSessions({
280
- workspaceId,
281
- taskId: '',
282
- imageAssetId: 'asset-image-sync-1'
283
- })).toEqual([
284
- expect.objectContaining({
285
- id: 'session-unattached-apply-1',
286
- taskId: '',
287
- baseImageAssetId: 'asset-image-sync-1',
288
- title: 'Synced loose notes'
289
- })
290
- ]);
291
- });
292
- it('returns 500 when annotated attachment sync persistence fails during apply-local', async () => {
293
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-annotated-failure-');
294
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
295
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
296
- vi.spyOn(core, 'upsertAnnotatedAttachmentSessionForSync').mockImplementation(() => {
297
- throw new Error('annotated sync failure');
298
- });
299
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
300
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
301
- expect(match).toBeTruthy();
302
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
303
- changes: [
304
- {
305
- op: 'annotated-attachment-session-upsert',
306
- session: {
307
- id: 'session-fail-1',
308
- workspaceId,
309
- taskId: '',
310
- baseImageAssetId: 'asset-image-sync-1',
311
- title: 'Broken notes',
312
- globalInstruction: null,
313
- annotations: [],
314
- createdByActorId: null,
315
- updatedByActorId: null,
316
- createdAt: '2026-03-30T20:00:00.000Z',
317
- updatedAt: '2026-03-30T20:05:00.000Z',
318
- deletedAt: null
319
- }
320
- }
321
- ]
322
- }, { 'x-taskforce-workspace-id': workspaceId });
323
- const res = createMockRes();
324
- await match?.handler(req, res, {});
325
- expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
326
- expect(readJsonBody(res)).toEqual(expect.objectContaining({
327
- error: expect.stringContaining('Failed to persist annotated attachment session session-fail-1 during sync apply')
328
- }));
329
- });
330
- it('persists synced ai profiles via apply-local route', async () => {
331
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-profiles-');
332
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
333
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
334
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
335
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
336
- expect(match).toBeTruthy();
337
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
338
- changes: [
339
- {
340
- op: 'ai-profile-upsert',
341
- profile: {
342
- id: 'ai-profile-codex',
343
- workspaceId,
344
- profileToken: 'tfp_codex_sync',
345
- name: 'Codex',
346
- username: 'codex',
347
- icon: 'Bot',
348
- color: '#0ea5e9',
349
- avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
350
- avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp',
351
- description: 'Terminal-first coding agent',
352
- role: 'Implementer',
353
- provider: 'OpenAI',
354
- model: 'gpt-5.4',
355
- surfaceType: 'coding_tool',
356
- createdAt: '2026-03-22T10:00:00.000Z',
357
- updatedAt: '2026-03-22T10:05:00.000Z',
358
- lastActiveAt: '2026-03-22T10:05:00.000Z'
359
- }
360
- }
361
- ]
362
- }, { 'x-taskforce-workspace-id': workspaceId });
363
- const res = createMockRes();
364
- await match?.handler(req, res, {});
365
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
366
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
367
- expect(reloadedCore.listAiProfiles(workspaceId)).toEqual([
368
- expect.objectContaining({
369
- id: 'ai-profile-codex',
370
- name: 'Codex',
371
- username: 'codex',
372
- description: 'Terminal-first coding agent',
373
- role: 'Implementer',
374
- provider: 'OpenAI',
375
- model: 'gpt-5.4',
376
- surfaceType: 'coding_tool',
377
- avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
378
- avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp'
379
- })
380
- ]);
381
- });
382
- it('rejects synced ai profiles with an invalid surfaceType via apply-local route', async () => {
383
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-profiles-invalid-surface-');
384
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
385
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
386
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
387
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
388
- expect(match).toBeTruthy();
389
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
390
- changes: [
391
- {
392
- op: 'ai-profile-upsert',
393
- profile: {
394
- id: 'ai-profile-invalid',
395
- workspaceId,
396
- profileToken: 'tfp_invalid_surface',
397
- name: 'OpenClaw Agent',
398
- username: 'openclaw-agent',
399
- icon: 'Bot',
400
- color: '#64748b',
401
- surfaceType: 'desktop_app',
402
- createdAt: '2026-03-22T10:00:00.000Z',
403
- updatedAt: '2026-03-22T10:05:00.000Z',
404
- lastActiveAt: '2026-03-22T10:05:00.000Z'
405
- }
406
- }
407
- ]
408
- }, { 'x-taskforce-workspace-id': workspaceId });
409
- const res = createMockRes();
410
- await match?.handler(req, res, {});
411
- expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
412
- expect(readJsonBody(res)).toEqual(expect.objectContaining({
413
- code: 'AI_PROFILE_SURFACE_TYPE_INVALID',
414
- error: expect.stringContaining('surfaceType')
415
- }));
416
- });
417
- it('applies tasks whose assignee resolves after ai profile sync', async () => {
418
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-assignee-');
419
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
420
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
421
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
422
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
423
- expect(match).toBeTruthy();
424
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
425
- changes: [
426
- {
427
- op: 'ai-profile-upsert',
428
- profile: {
429
- id: 'ai-profile-codex',
430
- workspaceId,
431
- profileToken: 'tfp_codex_sync',
432
- name: 'Codex',
433
- username: 'codex',
434
- icon: 'Bot',
435
- color: '#0ea5e9',
436
- surfaceType: 'coding_tool',
437
- createdAt: '2026-03-22T10:00:00.000Z',
438
- updatedAt: '2026-03-22T10:05:00.000Z',
439
- lastActiveAt: '2026-03-22T10:05:00.000Z'
440
- }
441
- },
442
- {
443
- op: 'upsert',
444
- archived: false,
445
- task: {
446
- id: 'task-ai-assignee-1',
447
- title: 'Task assigned to synced AI',
448
- status: 'task',
449
- assignee: 'ai-profile-codex'
450
- }
451
- }
452
- ]
453
- }, { 'x-taskforce-workspace-id': workspaceId });
454
- const res = createMockRes();
455
- await match?.handler(req, res, {});
456
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
457
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
458
- const task = reloadedCore.getTask('task-ai-assignee-1', workspaceId);
459
- expect(task?.assignee).toBe('ai-profile-codex');
460
- expect(task?.assigneeActor).toEqual(expect.objectContaining({
461
- id: 'ai-profile-codex',
462
- kind: 'ai',
463
- label: 'Codex'
464
- }));
465
- });
466
- it('applies tasks whose assignee resolves to a workspace member', async () => {
467
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-human-assignee-');
468
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
469
- seedAuthenticatedWorkspace(core, { workspaceId, userId: 'member-1', name: 'Local Active Workspace' });
470
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
471
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
472
- expect(match).toBeTruthy();
473
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
474
- changes: [
475
- {
476
- op: 'upsert',
477
- archived: false,
478
- task: {
479
- id: 'task-human-assignee-1',
480
- title: 'Task assigned to workspace member',
481
- status: 'task',
482
- assignee: 'member-1'
483
- }
484
- }
485
- ]
486
- }, { 'x-taskforce-workspace-id': workspaceId });
487
- const res = createMockRes();
488
- await match?.handler(req, res, {});
489
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
490
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
491
- const task = reloadedCore.getTask('task-human-assignee-1', workspaceId);
492
- expect(task?.assignee).toBe('member-1');
493
- expect(task?.assigneeActor).toEqual(expect.objectContaining({
494
- id: 'member-1',
495
- kind: 'human'
496
- }));
497
- });
498
- it('preserves unresolved human assignees during apply-local sync', async () => {
499
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-unresolved-human-assignee-');
500
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
501
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
502
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
503
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
504
- expect(match).toBeTruthy();
505
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
506
- changes: [
507
- {
508
- op: 'upsert',
509
- archived: false,
510
- task: {
511
- id: 'task-unresolved-human-assignee-1',
512
- title: 'Task assigned to remote human',
513
- status: 'task',
514
- assignee: 'user-019c91d9-c143-771d-b0bf-36e318cfb353'
515
- }
516
- }
517
- ]
518
- }, { 'x-taskforce-workspace-id': workspaceId });
519
- const res = createMockRes();
520
- await match?.handler(req, res, {});
521
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
522
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
523
- const task = reloadedCore.getTask('task-unresolved-human-assignee-1', workspaceId);
524
- expect(task?.assignee).toBe('user-019c91d9-c143-771d-b0bf-36e318cfb353');
525
- expect(task?.assigneeActor).toEqual(expect.objectContaining({
526
- id: 'user-019c91d9-c143-771d-b0bf-36e318cfb353',
527
- kind: 'human',
528
- meta: expect.objectContaining({ unresolved: true })
529
- }));
530
- });
531
- it('still rejects unresolved ai assignees during apply-local sync', async () => {
532
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-unresolved-ai-assignee-');
533
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
534
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
535
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
536
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
537
- expect(match).toBeTruthy();
538
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
539
- changes: [
540
- {
541
- op: 'upsert',
542
- archived: false,
543
- task: {
544
- id: 'task-unresolved-ai-assignee-1',
545
- title: 'Task assigned to missing AI profile',
546
- status: 'task',
547
- assignee: 'ai-profile-missing'
548
- }
549
- }
550
- ]
551
- }, { 'x-taskforce-workspace-id': workspaceId });
552
- const res = createMockRes();
553
- await match?.handler(req, res, {});
554
- expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
555
- expect(res.end).toHaveBeenCalledWith(expect.stringContaining('references unresolved assignee ai-profile-missing'));
556
- });
557
- it('persists archived cloud-pulled tasks into local sqlite archive via apply-local route', async () => {
558
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-archived-');
559
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
560
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
561
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
562
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
563
- expect(match).toBeTruthy();
564
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
565
- changes: [
566
- {
567
- op: 'upsert',
568
- archived: true,
569
- task: {
570
- id: 'task-cloud-archived-1',
571
- title: 'Archived from cloud',
572
- status: 'done',
573
- isArchived: true
574
- }
575
- }
576
- ]
577
- }, { 'x-taskforce-workspace-id': workspaceId });
578
- const res = createMockRes();
579
- await match?.handler(req, res, {});
580
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
581
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
582
- const persistedArchive = reloadedCore.listArchive(workspaceId);
583
- const archivedTask = persistedArchive.archived.find((item) => item.id === 'task-cloud-archived-1');
584
- const defaultWorkspaceArchive = reloadedCore.listArchive('default').archived;
585
- expect(archivedTask).toBeTruthy();
586
- expect(archivedTask?.title).toBe('Archived from cloud');
587
- expect(archivedTask?.isArchived).toBe(true);
588
- expect(defaultWorkspaceArchive.find((item) => item.id === 'task-cloud-archived-1')).toBeUndefined();
589
- });
590
- it('persists archived task workstream links into local sqlite archive via apply-local route', async () => {
591
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-archived-workstream-');
592
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
593
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
594
- core.createInitiative({
595
- id: 'initiative-archived-sync-1',
596
- title: 'Archived synced initiative',
597
- createdAt: '2026-03-15T00:00:00.000Z',
598
- updatedAt: '2026-03-15T00:00:00.000Z'
599
- }, workspaceId);
600
- core.createWorkstream({
601
- id: 'workstream-archived-sync-1',
602
- initiativeId: 'initiative-archived-sync-1',
603
- title: 'Archived synced workstream',
604
- createdAt: '2026-03-15T00:00:00.000Z',
605
- updatedAt: '2026-03-15T00:00:00.000Z'
606
- }, workspaceId);
607
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
608
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
609
- expect(match).toBeTruthy();
610
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
611
- changes: [
612
- {
613
- op: 'upsert',
614
- archived: true,
615
- task: {
616
- id: 'task-cloud-archived-linked-1',
617
- title: 'Archived linked task',
618
- status: 'done',
619
- workstreamId: 'workstream-archived-sync-1',
620
- isArchived: true,
621
- updatedAt: '2026-03-16T00:27:08.000Z'
622
- }
623
- }
624
- ]
625
- }, { 'x-taskforce-workspace-id': workspaceId });
626
- const res = createMockRes();
627
- await match?.handler(req, res, {});
628
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
629
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
630
- const persistedArchive = reloadedCore.listArchive(workspaceId);
631
- const archivedTask = persistedArchive.archived.find((item) => item.id === 'task-cloud-archived-linked-1');
632
- expect(archivedTask).toBeTruthy();
633
- expect(archivedTask?.isArchived).toBe(true);
634
- expect(archivedTask?.workstreamId).toBe('workstream-archived-sync-1');
635
- });
636
- it('applies multiple terminal sync updates in one batch', async () => {
637
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-terminal-status-');
638
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
639
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
640
- core.createTask({
641
- id: 'task-terminal-1',
642
- title: 'Terminal task 1',
643
- status: 'task',
644
- createdAt: '2026-03-15T00:00:00.000Z',
645
- updatedAt: '2026-03-15T00:00:00.000Z'
646
- }, {
647
- workspaceId,
648
- allowClientProvidedId: true
649
- });
650
- core.createTask({
651
- id: 'task-terminal-2',
652
- title: 'Terminal task 2',
653
- status: 'task',
654
- createdAt: '2026-03-15T00:00:00.000Z',
655
- updatedAt: '2026-03-15T00:00:00.000Z'
656
- }, {
657
- workspaceId,
658
- allowClientProvidedId: true
659
- });
660
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
661
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
662
- expect(match).toBeTruthy();
663
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
664
- changes: [
665
- {
666
- op: 'upsert',
667
- archived: false,
668
- task: {
669
- id: 'task-terminal-1',
670
- title: 'Terminal task 1',
671
- status: 'done',
672
- updatedAt: '2026-03-16T00:27:08.000Z'
673
- }
674
- },
675
- {
676
- op: 'upsert',
677
- archived: false,
678
- task: {
679
- id: 'task-terminal-2',
680
- title: 'Terminal task 2',
681
- status: 'done',
682
- updatedAt: '2026-03-16T00:27:08.000Z'
683
- }
684
- }
685
- ]
686
- }, { 'x-taskforce-workspace-id': workspaceId });
687
- const res = createMockRes();
688
- await match?.handler(req, res, {});
689
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
690
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
691
- const first = reloadedCore.getTask('task-terminal-1', workspaceId);
692
- const second = reloadedCore.getTask('task-terminal-2', workspaceId);
693
- expect(first?.status).toBe('done');
694
- expect(second?.status).toBe('done');
695
- });
696
- it('preserves task workstream links when applying a synced snapshot', async () => {
697
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-workstream-link-');
698
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
699
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
700
- core.createInitiative({
701
- id: 'initiative-sync-1',
702
- title: 'Synced initiative',
703
- createdAt: '2026-03-15T00:00:00.000Z',
704
- updatedAt: '2026-03-15T00:00:00.000Z'
705
- }, workspaceId);
706
- core.createWorkstream({
707
- id: 'workstream-sync-1',
708
- initiativeId: 'initiative-sync-1',
709
- title: 'Synced workstream',
710
- createdAt: '2026-03-15T00:00:00.000Z',
711
- updatedAt: '2026-03-15T00:00:00.000Z'
712
- }, workspaceId);
713
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
714
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
715
- expect(match).toBeTruthy();
716
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
717
- changes: [
718
- {
719
- op: 'upsert',
720
- archived: false,
721
- task: {
722
- id: 'task-workstream-linked-1',
723
- title: 'Linked task',
724
- status: 'task',
725
- workstreamId: 'workstream-sync-1',
726
- updatedAt: '2026-03-16T00:27:08.000Z'
727
- }
728
- }
729
- ]
730
- }, { 'x-taskforce-workspace-id': workspaceId });
731
- const res = createMockRes();
732
- await match?.handler(req, res, {});
733
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
734
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
735
- const task = reloadedCore.getTask('task-workstream-linked-1', workspaceId);
736
- expect(task?.workstreamId).toBe('workstream-sync-1');
737
- });
738
- it('persists checklist items when applying a synced snapshot locally', async () => {
739
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-checklist-apply-');
740
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
741
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
742
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
743
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
744
- expect(match).toBeTruthy();
745
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
746
- changes: [
747
- {
748
- op: 'upsert',
749
- archived: false,
750
- task: {
751
- id: 'task-checklist-sync-1',
752
- title: 'Checklist sync task',
753
- status: 'task',
754
- updatedAt: '2026-03-16T00:27:08.000Z',
755
- checklistItems: [
756
- {
757
- id: 'checklist-sync-1',
758
- title: 'First synced item',
759
- isCompleted: false,
760
- order: 0,
761
- createdAt: '2026-03-16T00:00:00.000Z',
762
- updatedAt: '2026-03-16T00:27:08.000Z',
763
- },
764
- {
765
- id: 'checklist-sync-2',
766
- title: 'Second synced item',
767
- isCompleted: true,
768
- order: 1,
769
- createdAt: '2026-03-16T00:00:00.000Z',
770
- updatedAt: '2026-03-16T00:27:08.000Z',
771
- }
772
- ]
773
- }
774
- }
775
- ]
776
- }, { 'x-taskforce-workspace-id': workspaceId });
777
- const res = createMockRes();
778
- await match?.handler(req, res, {});
779
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
780
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
781
- const task = reloadedCore.getTask('task-checklist-sync-1', workspaceId);
782
- expect(task?.checklistItems?.map((item) => item.title)).toEqual([
783
- 'First synced item',
784
- 'Second synced item',
785
- ]);
786
- expect(task?.checklistItems?.map((item) => item.isCompleted)).toEqual([false, true]);
787
- });
788
- it('serves synced workstream links back through the live cloud tasks route after push', async () => {
789
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-workstream-route-');
790
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
791
- seedAuthenticatedWorkspace(core);
792
- core.createInitiative({
793
- id: 'initiative-cloud-1',
794
- title: 'Cloud initiative',
795
- createdAt: '2026-03-15T00:00:00.000Z',
796
- updatedAt: '2026-03-15T00:00:00.000Z'
797
- }, workspaceId);
798
- core.createWorkstream({
799
- id: 'workstream-cloud-1',
800
- initiativeId: 'initiative-cloud-1',
801
- title: 'Cloud workstream',
802
- createdAt: '2026-03-15T00:00:00.000Z',
803
- updatedAt: '2026-03-15T00:00:00.000Z'
804
- }, workspaceId);
805
- const routes = createRoutes(core, { authConfig });
806
- const cookie = createAuthCookie(workspaceId);
807
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
808
- expect(pushMatch).toBeTruthy();
809
- const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
810
- workspaceId,
811
- idempotencyKey: 'push-cloud-workstream-link-1',
812
- changes: [
813
- {
814
- op: 'upsert',
815
- archived: false,
816
- task: {
817
- id: 'task-cloud-workstream-1',
818
- title: 'Cloud linked task',
819
- status: 'task',
820
- workstreamId: 'workstream-cloud-1',
821
- updatedAt: '2026-03-16T00:27:08.000Z'
822
- }
823
- }
824
- ]
825
- }, { cookie });
826
- const pushRes = createMockRes();
827
- await pushMatch?.handler(pushReq, pushRes, {});
828
- expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
829
- const tasksMatch = matchRoute('GET', '/api/taskforce/tasks', routes);
830
- expect(tasksMatch).toBeTruthy();
831
- const tasksReq = createMockReq('GET', `/api/taskforce/tasks?workspaceId=${workspaceId}`, undefined, { cookie });
832
- const tasksRes = createMockRes();
833
- await tasksMatch?.handler(tasksReq, tasksRes, {});
834
- expect(tasksRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
835
- const payload = readJsonBody(tasksRes);
836
- const task = Array.isArray(payload?.tasks)
837
- ? payload.tasks.find((entry) => entry?.id === 'task-cloud-workstream-1')
838
- : null;
839
- expect(task).toBeTruthy();
840
- expect(task?.workstreamId).toBe('workstream-cloud-1');
841
- });
842
- it('prefers incoming workstream links when timestamps tie but task relationships differ', async () => {
843
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-workstream-tie-');
844
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
845
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
846
- core.createInitiative({
847
- id: 'initiative-sync-2',
848
- title: 'Tie-break initiative',
849
- createdAt: '2026-03-15T00:00:00.000Z',
850
- updatedAt: '2026-03-15T00:00:00.000Z'
851
- }, workspaceId);
852
- core.createWorkstream({
853
- id: 'workstream-sync-2',
854
- initiativeId: 'initiative-sync-2',
855
- title: 'Tie-break workstream',
856
- createdAt: '2026-03-15T00:00:00.000Z',
857
- updatedAt: '2026-03-15T00:00:00.000Z'
858
- }, workspaceId);
859
- core.createTask({
860
- id: 'task-workstream-linked-2',
861
- title: 'Linked task',
862
- status: 'task',
863
- updatedAt: '2026-03-16T00:27:08.000Z',
864
- createdAt: '2026-03-16T00:00:00.000Z'
865
- }, {
866
- workspaceId,
867
- allowClientProvidedId: true,
868
- syncUpdatedAt: '2026-03-16T00:27:08.000Z',
869
- });
870
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
871
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
872
- expect(match).toBeTruthy();
873
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
874
- changes: [
875
- {
876
- op: 'upsert',
877
- archived: false,
878
- task: {
879
- id: 'task-workstream-linked-2',
880
- title: 'Linked task',
881
- status: 'task',
882
- workstreamId: 'workstream-sync-2',
883
- updatedAt: '2026-03-16T00:27:08.000Z',
884
- createdAt: '2026-03-16T00:00:00.000Z'
885
- }
886
- }
887
- ]
888
- }, { 'x-taskforce-workspace-id': workspaceId });
889
- const res = createMockRes();
890
- await match?.handler(req, res, {});
891
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
892
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
893
- const task = reloadedCore.getTask('task-workstream-linked-2', workspaceId);
894
- expect(task?.workstreamId).toBe('workstream-sync-2');
895
- });
896
- it('prefers incoming checklist changes when timestamps tie but checklist content differs', async () => {
897
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-checklist-tie-');
898
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
899
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
900
- core.createTask({
901
- id: 'task-checklist-sync-2',
902
- title: 'Checklist tie-break task',
903
- status: 'task',
904
- updatedAt: '2026-03-16T00:27:08.000Z',
905
- createdAt: '2026-03-16T00:00:00.000Z',
906
- checklistItems: [
907
- {
908
- id: 'checklist-existing-1',
909
- taskId: 'task-checklist-sync-2',
910
- title: 'Old local item',
911
- isCompleted: false,
912
- order: 0,
913
- createdAt: '2026-03-16T00:00:00.000Z',
914
- updatedAt: '2026-03-16T00:27:08.000Z',
915
- }
916
- ]
917
- }, { workspaceId, allowClientProvidedId: true });
918
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
919
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
920
- expect(match).toBeTruthy();
921
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
922
- changes: [
923
- {
924
- op: 'upsert',
925
- archived: false,
926
- task: {
927
- id: 'task-checklist-sync-2',
928
- title: 'Checklist tie-break task',
929
- status: 'task',
930
- updatedAt: '2026-03-16T00:27:08.000Z',
931
- checklistItems: [
932
- {
933
- id: 'checklist-incoming-1',
934
- title: 'Incoming synced item',
935
- isCompleted: true,
936
- order: 0,
937
- createdAt: '2026-03-16T00:00:00.000Z',
938
- updatedAt: '2026-03-16T00:27:08.000Z',
939
- }
940
- ]
941
- }
942
- }
943
- ]
944
- }, { 'x-taskforce-workspace-id': workspaceId });
945
- const res = createMockRes();
946
- await match?.handler(req, res, {});
947
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
948
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
949
- const task = reloadedCore.getTask('task-checklist-sync-2', workspaceId);
950
- expect(task?.checklistItems?.map((item) => item.title)).toEqual(['Incoming synced item']);
951
- expect(task?.checklistItems?.map((item) => item.isCompleted)).toEqual([true]);
952
- });
953
- it('does not let an older pulled snapshot revert a newer local in-progress update', async () => {
954
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-newer-local-status-');
955
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
956
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
957
- core.createTask({
958
- id: 'task-status-1',
959
- title: 'Task status',
960
- status: 'task',
961
- createdAt: '2026-03-16T00:00:00.000Z'
962
- }, {
963
- workspaceId,
964
- allowClientProvidedId: true
965
- });
966
- const locallyUpdated = core.updateTask('task-status-1', {
967
- status: 'in-progress'
968
- }, workspaceId, { persistBackup: false });
969
- expect(locallyUpdated?.status).toBe('in-progress');
970
- expect(String(locallyUpdated?.updatedAt || '')).not.toBe('');
971
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
972
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
973
- expect(match).toBeTruthy();
974
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
975
- changes: [
976
- {
977
- op: 'upsert',
978
- archived: false,
979
- task: {
980
- id: 'task-status-1',
981
- title: 'Task status',
982
- status: 'task',
983
- updatedAt: '2026-03-16T00:00:01.000Z'
984
- }
985
- }
986
- ]
987
- }, { 'x-taskforce-workspace-id': workspaceId });
988
- const res = createMockRes();
989
- await match?.handler(req, res, {});
990
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
991
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
992
- const persisted = reloadedCore.getTask('task-status-1', workspaceId);
993
- expect(persisted?.status).toBe('in-progress');
994
- expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse('2026-03-16T00:00:01.000Z'));
995
- });
996
- it('applies cloud delete operations to local sqlite across reload via apply-local route', async () => {
997
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-delete-');
998
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
999
- seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1000
- seedCore.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
1001
- const seeded = seedCore.createTask({
1002
- title: 'Will be deleted by cloud sync',
1003
- category: 'Default',
1004
- type: 'feature',
1005
- priority: 2,
1006
- status: 'task'
1007
- }, { workspaceId });
1008
- expect(seeded.id).toBeTruthy();
1009
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1010
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1011
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1012
- expect(match).toBeTruthy();
1013
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1014
- changes: [
1015
- { op: 'delete', taskId: seeded.id }
1016
- ]
1017
- }, { 'x-taskforce-workspace-id': workspaceId });
1018
- const res = createMockRes();
1019
- await match?.handler(req, res, {});
1020
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1021
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1022
- const persisted = reloadedCore.listTasks(workspaceId);
1023
- const deletedTask = persisted.tasks.find((item) => item.id === seeded.id);
1024
- expect(deletedTask).toBeUndefined();
1025
- });
1026
- it('keeps task deleted when apply-local receives same-task upsert and delete in one batch', async () => {
1027
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-delete-wins-');
1028
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1029
- seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1030
- seedCore.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
1031
- const seeded = seedCore.createTask({
1032
- title: 'Seed task for delete-wins',
1033
- category: 'Default',
1034
- type: 'feature',
1035
- priority: 2,
1036
- status: 'task'
1037
- }, { workspaceId });
1038
- expect(seeded.id).toBeTruthy();
1039
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1040
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1041
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1042
- expect(match).toBeTruthy();
1043
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1044
- changes: [
1045
- {
1046
- op: 'upsert',
1047
- archived: false,
1048
- task: {
1049
- id: seeded.id,
1050
- title: 'Stale cloud upsert'
1051
- }
1052
- },
1053
- { op: 'delete', taskId: seeded.id }
1054
- ]
1055
- }, { 'x-taskforce-workspace-id': workspaceId });
1056
- const res = createMockRes();
1057
- await match?.handler(req, res, {});
1058
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1059
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1060
- const persisted = reloadedCore.listTasks(workspaceId);
1061
- const shouldRemainDeleted = persisted.tasks.find((item) => item.id === seeded.id);
1062
- expect(shouldRemainDeleted).toBeUndefined();
1063
- });
1064
- it('applies document-delete operations by deleting file and clearing watermark index', async () => {
1065
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-delete-');
1066
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1067
- seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1068
- const syncRoot = seedCore.getPaths().basePath;
1069
- const docsDir = path.join(syncRoot, 'docs');
1070
- fs.mkdirSync(docsDir, { recursive: true });
1071
- const relativeDocPath = 'docs/sync-delete.md';
1072
- const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1073
- fs.writeFileSync(absoluteDocPath, '# sync delete\n', 'utf8');
1074
- seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
1075
- expect(seedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeTruthy();
1076
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1077
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1078
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1079
- expect(match).toBeTruthy();
1080
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1081
- changes: [
1082
- { op: 'document-delete', path: relativeDocPath }
1083
- ]
1084
- }, { 'x-taskforce-workspace-id': workspaceId });
1085
- const res = createMockRes();
1086
- await match?.handler(req, res, {});
1087
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1088
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1089
- expect(fs.existsSync(absoluteDocPath)).toBe(false);
1090
- expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
1091
- });
1092
- it('persists canonical markdown asset metadata across reload via apply-local route', async () => {
1093
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-asset-');
1094
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1095
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1096
- core.createTask({
1097
- id: 'task-doc-asset-1',
1098
- title: 'Task with synced markdown asset',
1099
- status: 'task'
1100
- }, {
1101
- workspaceId,
1102
- allowClientProvidedId: true
1103
- });
1104
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1105
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1106
- expect(match).toBeTruthy();
1107
- const relativeDocPath = 'docs/phase1-sync.md';
1108
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1109
- changes: [
1110
- {
1111
- op: 'document-upsert',
1112
- path: relativeDocPath,
1113
- updatedAt: '2026-03-17T12:00:00.000Z',
1114
- content: '# canonical markdown\n',
1115
- assetId: 'asset-doc-sync-1',
1116
- documentId: 'doc-sync-1',
1117
- version: 3,
1118
- taskId: 'task-doc-asset-1',
1119
- originalFilename: 'phase1-sync.md',
1120
- linkRole: 'attachment'
1121
- }
1122
- ]
1123
- }, { 'x-taskforce-workspace-id': workspaceId });
1124
- const res = createMockRes();
1125
- await match?.handler(req, res, {});
1126
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1127
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1128
- const absoluteDocPath = path.join(reloadedCore.getPaths().basePath, relativeDocPath);
1129
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1130
- const asset = store.get('asset-doc-sync-1', workspaceId);
1131
- const links = store.listLinksForAsset('asset-doc-sync-1', workspaceId);
1132
- expect(fs.readFileSync(absoluteDocPath, 'utf8')).toBe('# canonical markdown\n');
1133
- expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toEqual({
1134
- updatedAt: '2026-03-17T12:00:00.000Z',
1135
- contentHash: expect.any(String)
1136
- });
1137
- expect(asset).toEqual(expect.objectContaining({
1138
- assetId: 'asset-doc-sync-1',
1139
- workspaceId,
1140
- kind: 'document',
1141
- storageProvider: 'local',
1142
- storageKey: relativeDocPath,
1143
- documentId: 'doc-sync-1',
1144
- version: 3,
1145
- originalFilename: 'phase1-sync.md',
1146
- mimeType: 'text/markdown',
1147
- deletedAt: null
1148
- }));
1149
- expect(links).toEqual([
1150
- expect.objectContaining({
1151
- assetId: 'asset-doc-sync-1',
1152
- workspaceId,
1153
- taskId: 'task-doc-asset-1',
1154
- role: 'attachment',
1155
- deletedAt: null
1156
- })
1157
- ]);
1158
- });
1159
- it('retries document asset reference allocation during apply-local when persistence hits a collision', async () => {
1160
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-asset-collision-');
1161
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1162
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1163
- core.createTask({
1164
- id: 'task-doc-asset-collision-1',
1165
- title: 'Task with collided synced markdown asset',
1166
- status: 'task'
1167
- }, {
1168
- workspaceId,
1169
- allowClientProvidedId: true
1170
- });
1171
- const db = core.getDatabaseAdapter();
1172
- const seededStore = new WorkspaceAssetStore(db, core.getTenantId());
1173
- seededStore.upsertAsset({
1174
- assetId: 'asset-doc-existing-collision',
1175
- workspaceId,
1176
- kind: 'document',
1177
- originalFilename: 'existing.md',
1178
- mimeType: 'text/markdown',
1179
- storageProvider: 'local',
1180
- storageKey: 'docs/existing-collision.md',
1181
- contentSha256: 'sha-existing-collision',
1182
- sizeBytes: 10,
1183
- referenceNumber: 7,
1184
- createdAt: '2026-03-17T11:55:00.000Z',
1185
- updatedAt: '2026-03-17T11:55:00.000Z'
1186
- });
1187
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1188
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1189
- expect(match).toBeTruthy();
1190
- const relativeDocPath = 'docs/phase1-sync-collision.md';
1191
- const originalPrepare = db.prepare.bind(db);
1192
- let forcedCollision = false;
1193
- vi.spyOn(db, 'prepare').mockImplementation((...args) => {
1194
- const [sql] = args;
1195
- const statement = originalPrepare(...args);
1196
- if (typeof sql !== 'string') {
1197
- return statement;
1198
- }
1199
- if (!sql.includes('INSERT INTO workspace_assets')) {
1200
- return statement;
1201
- }
1202
- return {
1203
- ...statement,
1204
- run: (...args) => {
1205
- const storageKeyArg = args[8];
1206
- const referenceNumberArg = args[12];
1207
- if (!forcedCollision
1208
- && storageKeyArg === relativeDocPath
1209
- && referenceNumberArg === 8) {
1210
- forcedCollision = true;
1211
- throw new Error('UNIQUE constraint failed: workspace_assets.tenant_id, workspace_assets.workspace_id, workspace_assets.kind, workspace_assets.reference_number');
1212
- }
1213
- return statement.run(...args);
1214
- }
1215
- };
1216
- });
1217
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1218
- changes: [
1219
- {
1220
- op: 'document-upsert',
1221
- path: relativeDocPath,
1222
- updatedAt: '2026-03-17T12:00:00.000Z',
1223
- content: '# canonical markdown after collision\n',
1224
- assetId: 'asset-doc-sync-collision-1',
1225
- documentId: 'doc-sync-collision-1',
1226
- referenceNumber: 8,
1227
- version: 3,
1228
- taskId: 'task-doc-asset-collision-1',
1229
- originalFilename: 'phase1-sync-collision.md',
1230
- linkRole: 'attachment'
1231
- }
1232
- ]
1233
- }, { 'x-taskforce-workspace-id': workspaceId });
1234
- const res = createMockRes();
1235
- await match?.handler(req, res, {});
1236
- expect(forcedCollision).toBe(true);
1237
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1238
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1239
- const absoluteDocPath = path.join(reloadedCore.getPaths().basePath, relativeDocPath);
1240
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1241
- const asset = store.get('asset-doc-sync-collision-1', workspaceId);
1242
- const links = store.listLinksForAsset('asset-doc-sync-collision-1', workspaceId);
1243
- const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1244
- expect(fs.readFileSync(absoluteDocPath, 'utf8')).toBe('# canonical markdown after collision\n');
1245
- expect(asset).toEqual(expect.objectContaining({
1246
- assetId: 'asset-doc-sync-collision-1',
1247
- workspaceId,
1248
- kind: 'document',
1249
- storageKey: relativeDocPath,
1250
- documentId: 'doc-sync-collision-1',
1251
- version: 3,
1252
- referenceNumber: 9,
1253
- deletedAt: null
1254
- }));
1255
- expect(links).toEqual([
1256
- expect.objectContaining({
1257
- assetId: 'asset-doc-sync-collision-1',
1258
- workspaceId,
1259
- taskId: 'task-doc-asset-collision-1',
1260
- role: 'attachment',
1261
- deletedAt: null
1262
- })
1263
- ]);
1264
- expect(syncEvents.some((event) => event.errorMessage === 'Failed to persist document watermark after write.')).toBe(false);
1265
- });
1266
- it('persists canonical binary assets across reload via apply-local route', async () => {
1267
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-binary-asset-');
1268
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1269
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1270
- core.createTask({
1271
- id: 'task-binary-asset-1',
1272
- title: 'Task with synced binary asset',
1273
- status: 'task'
1274
- }, {
1275
- workspaceId,
1276
- allowClientProvidedId: true
1277
- });
1278
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1279
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1280
- expect(match).toBeTruthy();
1281
- const relativeAssetPath = 'workspaces/workspace-local-active/assets/images/asset-binary-sync-1-synced-image.png';
1282
- const assetBuffer = Buffer.from('binary-image-content');
1283
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1284
- changes: [
1285
- {
1286
- op: 'asset-upsert',
1287
- path: relativeAssetPath,
1288
- updatedAt: '2026-03-17T12:05:00.000Z',
1289
- contentBase64: assetBuffer.toString('base64'),
1290
- assetId: 'asset-binary-sync-1',
1291
- kind: 'image',
1292
- mimeType: 'image/png',
1293
- taskId: 'task-binary-asset-1',
1294
- originalFilename: 'synced-image.png',
1295
- linkRole: 'image'
1296
- }
1297
- ]
1298
- }, { 'x-taskforce-workspace-id': workspaceId });
1299
- const res = createMockRes();
1300
- await match?.handler(req, res, {});
1301
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1302
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1303
- const absoluteAssetPath = path.join(reloadedCore.getPaths().basePath, relativeAssetPath);
1304
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1305
- const asset = store.get('asset-binary-sync-1', workspaceId);
1306
- const links = store.listLinksForAsset('asset-binary-sync-1', workspaceId);
1307
- expect(fs.readFileSync(absoluteAssetPath)).toEqual(assetBuffer);
1308
- expect(asset).toEqual(expect.objectContaining({
1309
- assetId: 'asset-binary-sync-1',
1310
- workspaceId,
1311
- kind: 'image',
1312
- storageProvider: 'local',
1313
- storageKey: relativeAssetPath,
1314
- originalFilename: 'synced-image.png',
1315
- mimeType: 'image/png',
1316
- deletedAt: null
1317
- }));
1318
- expect(links).toEqual([
1319
- expect.objectContaining({
1320
- assetId: 'asset-binary-sync-1',
1321
- workspaceId,
1322
- taskId: 'task-binary-asset-1',
1323
- role: 'image',
1324
- deletedAt: null
1325
- })
1326
- ]);
1327
- });
1328
- it('lets the cloud document ref win during normal sync apply without recording a sync error', async () => {
1329
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
1330
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1331
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1332
- core.createTask({
1333
- id: 'task-doc-ref-drift-1',
1334
- title: 'Task with drifting synced markdown asset',
1335
- status: 'task'
1336
- }, {
1337
- workspaceId,
1338
- allowClientProvidedId: true
1339
- });
1340
- const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1341
- seededStore.upsertAsset({
1342
- assetId: 'asset-doc-ref-drift-1',
1343
- workspaceId,
1344
- kind: 'document',
1345
- originalFilename: 'phase1-ref-drift.md',
1346
- mimeType: 'text/markdown',
1347
- storageProvider: 'local',
1348
- storageKey: 'docs/phase1-ref-drift.md',
1349
- contentSha256: 'sha-doc-ref-drift-local',
1350
- sizeBytes: 10,
1351
- documentId: 'doc-ref-drift-1',
1352
- referenceNumber: 12,
1353
- createdAt: '2026-03-17T11:55:00.000Z',
1354
- updatedAt: '2026-03-17T11:55:00.000Z'
1355
- });
1356
- seededStore.upsertLink({
1357
- workspaceId,
1358
- assetId: 'asset-doc-ref-drift-1',
1359
- taskId: 'task-doc-ref-drift-1',
1360
- role: 'attachment',
1361
- updatedAt: '2026-03-17T11:55:00.000Z',
1362
- deletedAt: null
1363
- });
1364
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1365
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1366
- expect(match).toBeTruthy();
1367
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1368
- changes: [
1369
- {
1370
- op: 'document-upsert',
1371
- path: 'docs/phase1-ref-drift.md',
1372
- updatedAt: '2026-03-17T12:00:00.000Z',
1373
- content: '# cloud canonical markdown\n',
1374
- assetId: 'asset-doc-ref-drift-1',
1375
- documentId: 'doc-ref-drift-1',
1376
- referenceNumber: 18,
1377
- version: 2,
1378
- taskId: 'task-doc-ref-drift-1',
1379
- originalFilename: 'phase1-ref-drift.md',
1380
- linkRole: 'attachment'
1381
- }
1382
- ]
1383
- }, { 'x-taskforce-workspace-id': workspaceId });
1384
- const res = createMockRes();
1385
- await match?.handler(req, res, {});
1386
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1387
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1388
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1389
- const asset = store.get('asset-doc-ref-drift-1', workspaceId);
1390
- const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1391
- && event.status === 'error'
1392
- && event.errorMessage === 'Detected document reference number mismatch during normal sync.');
1393
- expect(asset).toEqual(expect.objectContaining({
1394
- assetId: 'asset-doc-ref-drift-1',
1395
- referenceNumber: 18,
1396
- version: 2
1397
- }));
1398
- expect(mismatchEvents).toHaveLength(0);
1399
- });
1400
- it('lets the cloud image ref win during normal sync apply without recording a sync error', async () => {
1401
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
1402
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1403
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1404
- core.createTask({
1405
- id: 'task-image-ref-drift-1',
1406
- title: 'Task with drifting synced image asset',
1407
- status: 'task'
1408
- }, {
1409
- workspaceId,
1410
- allowClientProvidedId: true
1411
- });
1412
- const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1413
- seededStore.upsertAsset({
1414
- assetId: 'asset-image-ref-drift-1',
1415
- workspaceId,
1416
- kind: 'image',
1417
- originalFilename: 'hero.png',
1418
- mimeType: 'image/png',
1419
- storageProvider: 'local',
1420
- storageKey: 'workspaces/workspace-local-active/assets/images/hero.png',
1421
- contentSha256: 'sha-image-ref-drift-local',
1422
- sizeBytes: 10,
1423
- referenceNumber: 12,
1424
- createdAt: '2026-03-17T11:55:00.000Z',
1425
- updatedAt: '2026-03-17T11:55:00.000Z'
1426
- });
1427
- seededStore.upsertLink({
1428
- workspaceId,
1429
- assetId: 'asset-image-ref-drift-1',
1430
- taskId: 'task-image-ref-drift-1',
1431
- role: 'image',
1432
- updatedAt: '2026-03-17T11:55:00.000Z',
1433
- deletedAt: null
1434
- });
1435
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1436
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1437
- expect(match).toBeTruthy();
1438
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1439
- changes: [
1440
- {
1441
- op: 'asset-upsert',
1442
- path: 'workspaces/workspace-local-active/assets/images/hero.png',
1443
- updatedAt: '2026-03-17T12:05:00.000Z',
1444
- contentBase64: Buffer.from('cloud-hero').toString('base64'),
1445
- assetId: 'asset-image-ref-drift-1',
1446
- kind: 'image',
1447
- mimeType: 'image/png',
1448
- referenceNumber: 18,
1449
- taskId: 'task-image-ref-drift-1',
1450
- originalFilename: 'hero.png',
1451
- linkRole: 'image'
1452
- }
1453
- ]
1454
- }, { 'x-taskforce-workspace-id': workspaceId });
1455
- const res = createMockRes();
1456
- await match?.handler(req, res, {});
1457
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1458
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1459
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1460
- const asset = store.get('asset-image-ref-drift-1', workspaceId);
1461
- const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1462
- && event.status === 'error'
1463
- && event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
1464
- expect(asset).toEqual(expect.objectContaining({
1465
- assetId: 'asset-image-ref-drift-1',
1466
- referenceNumber: 18
1467
- }));
1468
- expect(mismatchEvents).toHaveLength(0);
1469
- });
1470
- it('converges unchanged image reference drift during normal sync so later pulls stop re-logging it', async () => {
1471
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-converge-');
1472
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1473
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1474
- core.createTask({
1475
- id: 'task-image-ref-converge-1',
1476
- title: 'Task with converging synced image asset',
1477
- status: 'task'
1478
- }, {
1479
- workspaceId,
1480
- allowClientProvidedId: true
1481
- });
1482
- const basePath = core.getPaths().basePath;
1483
- const relativeAssetPath = 'workspaces/workspace-local-active/assets/images/hero-converge.png';
1484
- const absoluteAssetPath = path.join(basePath, relativeAssetPath);
1485
- fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
1486
- fs.writeFileSync(absoluteAssetPath, 'cloud-hero');
1487
- const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1488
- seededStore.upsertAsset({
1489
- assetId: 'asset-image-ref-converge-1',
1490
- workspaceId,
1491
- kind: 'image',
1492
- originalFilename: 'hero-converge.png',
1493
- mimeType: 'image/png',
1494
- storageProvider: 'local',
1495
- storageKey: relativeAssetPath,
1496
- contentSha256: createHash('sha256').update('cloud-hero').digest('hex'),
1497
- sizeBytes: Buffer.byteLength('cloud-hero'),
1498
- referenceNumber: 12,
1499
- createdAt: '2026-03-17T11:55:00.000Z',
1500
- updatedAt: '2026-03-17T12:05:00.000Z'
1501
- });
1502
- seededStore.upsertLink({
1503
- workspaceId,
1504
- assetId: 'asset-image-ref-converge-1',
1505
- taskId: 'task-image-ref-converge-1',
1506
- role: 'image',
1507
- updatedAt: '2026-03-17T11:55:00.000Z',
1508
- deletedAt: null
1509
- });
1510
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1511
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1512
- expect(match).toBeTruthy();
1513
- const body = {
1514
- changes: [
1515
- {
1516
- op: 'asset-upsert',
1517
- path: relativeAssetPath,
1518
- updatedAt: '2026-03-17T12:05:00.000Z',
1519
- contentBase64: Buffer.from('cloud-hero').toString('base64'),
1520
- assetId: 'asset-image-ref-converge-1',
1521
- kind: 'image',
1522
- mimeType: 'image/png',
1523
- referenceNumber: 18,
1524
- taskId: 'task-image-ref-converge-1',
1525
- originalFilename: 'hero-converge.png',
1526
- linkRole: 'image'
1527
- }
1528
- ]
1529
- };
1530
- const firstReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
1531
- const firstRes = createMockRes();
1532
- await match?.handler(firstReq, firstRes, {});
1533
- expect(firstRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1534
- const secondReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
1535
- const secondRes = createMockRes();
1536
- await match?.handler(secondReq, secondRes, {});
1537
- expect(secondRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1538
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1539
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1540
- const asset = store.get('asset-image-ref-converge-1', workspaceId);
1541
- const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1542
- && event.status === 'error'
1543
- && event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
1544
- expect(asset).toEqual(expect.objectContaining({
1545
- assetId: 'asset-image-ref-converge-1',
1546
- referenceNumber: 18
1547
- }));
1548
- expect(mismatchEvents).toHaveLength(0);
1549
- });
1550
- it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
1551
- const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
1552
- const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-local-');
1553
- const cloudCore = new TaskforceCore({ projectRoot: cloudProjectRoot, storagePath: '.taskforce' });
1554
- seedAuthenticatedWorkspace(cloudCore);
1555
- cloudCore.createTask({
1556
- id: 'task-cloud-link-roundtrip-1',
1557
- title: 'Cloud attachment source',
1558
- status: 'task'
1559
- }, {
1560
- workspaceId,
1561
- allowClientProvidedId: true
1562
- });
1563
- const cloudBasePath = cloudCore.getPaths().basePath;
1564
- const cloudStore = new WorkspaceAssetStore(cloudCore.getDatabaseAdapter(), cloudCore.getTenantId());
1565
- const markdownPath = `workspaces/${workspaceId}/assets/documents/asset-cloud-link-roundtrip-plan.md`;
1566
- const filePath = `workspaces/${workspaceId}/assets/files/asset-cloud-link-roundtrip-notes.txt`;
1567
- fs.mkdirSync(path.dirname(path.join(cloudBasePath, markdownPath)), { recursive: true });
1568
- fs.mkdirSync(path.dirname(path.join(cloudBasePath, filePath)), { recursive: true });
1569
- fs.writeFileSync(path.join(cloudBasePath, markdownPath), '# synced from cloud\n', 'utf8');
1570
- fs.writeFileSync(path.join(cloudBasePath, filePath), 'cloud text attachment', 'utf8');
1571
- cloudStore.upsertAsset({
1572
- assetId: 'asset-cloud-link-roundtrip-doc-1',
1573
- workspaceId,
1574
- kind: 'document',
1575
- originalFilename: 'plan.md',
1576
- mimeType: 'text/markdown',
1577
- storageProvider: 'local',
1578
- storageKey: markdownPath,
1579
- contentSha256: 'doc-roundtrip-hash',
1580
- sizeBytes: '# synced from cloud\n'.length,
1581
- documentId: 'doc-cloud-link-roundtrip-1',
1582
- version: 1,
1583
- isLatest: true,
1584
- createdAt: '2026-03-18T09:00:00.000Z',
1585
- updatedAt: '2026-03-18T09:00:00.000Z'
1586
- });
1587
- cloudStore.upsertLink({
1588
- workspaceId,
1589
- assetId: 'asset-cloud-link-roundtrip-doc-1',
1590
- taskId: 'task-cloud-link-roundtrip-1',
1591
- role: 'attachment',
1592
- updatedAt: '2026-03-18T10:00:00.000Z'
1593
- });
1594
- cloudStore.upsertAsset({
1595
- assetId: 'asset-cloud-link-roundtrip-file-1',
1596
- workspaceId,
1597
- kind: 'file',
1598
- originalFilename: 'notes.txt',
1599
- mimeType: 'text/plain',
1600
- storageProvider: 'local',
1601
- storageKey: filePath,
1602
- contentSha256: 'file-roundtrip-hash',
1603
- sizeBytes: 'cloud text attachment'.length,
1604
- isLatest: true,
1605
- createdAt: '2026-03-18T09:00:00.000Z',
1606
- updatedAt: '2026-03-18T09:00:00.000Z'
1607
- });
1608
- cloudStore.upsertLink({
1609
- workspaceId,
1610
- assetId: 'asset-cloud-link-roundtrip-file-1',
1611
- taskId: 'task-cloud-link-roundtrip-1',
1612
- role: 'attachment',
1613
- updatedAt: '2026-03-18T10:00:01.000Z'
1614
- });
1615
- const cloudRoutes = createRoutes(cloudCore, { authConfig });
1616
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', cloudRoutes);
1617
- expect(pullMatch).toBeTruthy();
1618
- const cookie = createAuthCookie(workspaceId);
1619
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=50&cursor=eyJ1IjoiMjAyNi0wMy0xOFQwOTozMDowMC4wMDBaIiwiaWQiOiJzZWVkIn0', undefined, { cookie });
1620
- const pullRes = createMockRes();
1621
- await pullMatch?.handler(pullReq, pullRes, {});
1622
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1623
- const pullBody = readJsonBody(pullRes);
1624
- const pulledChanges = Array.isArray(pullBody?.changes) ? pullBody.changes : [];
1625
- expect(pulledChanges).toEqual(expect.arrayContaining([
1626
- expect.objectContaining({
1627
- op: 'document-upsert',
1628
- path: markdownPath,
1629
- assetId: 'asset-cloud-link-roundtrip-doc-1',
1630
- taskId: 'task-cloud-link-roundtrip-1',
1631
- originalFilename: 'plan.md',
1632
- linkRole: 'attachment'
1633
- }),
1634
- expect.objectContaining({
1635
- op: 'asset-upsert',
1636
- path: filePath,
1637
- assetId: 'asset-cloud-link-roundtrip-file-1',
1638
- taskId: 'task-cloud-link-roundtrip-1',
1639
- originalFilename: 'notes.txt',
1640
- linkRole: 'attachment'
1641
- })
1642
- ]));
1643
- const localCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1644
- localCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1645
- localCore.createTask({
1646
- id: 'task-cloud-link-roundtrip-1',
1647
- title: 'Local attachment target',
1648
- status: 'task'
1649
- }, {
1650
- workspaceId,
1651
- allowClientProvidedId: true
1652
- });
1653
- const localRoutes = createRoutes(localCore, { authConfig: { runtimeMode: 'local', enabled: false } });
1654
- const applyMatch = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', localRoutes);
1655
- expect(applyMatch).toBeTruthy();
1656
- const applyReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: pulledChanges }, { 'x-taskforce-workspace-id': workspaceId });
1657
- const applyRes = createMockRes();
1658
- await applyMatch?.handler(applyReq, applyRes, {});
1659
- expect(applyRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1660
- const reloadedLocalCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1661
- const reloadedStore = new WorkspaceAssetStore(reloadedLocalCore.getDatabaseAdapter(), reloadedLocalCore.getTenantId());
1662
- expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, markdownPath), 'utf8')).toBe('# synced from cloud\n');
1663
- expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, filePath), 'utf8')).toBe('cloud text attachment');
1664
- expect(reloadedStore.listLinksForAsset('asset-cloud-link-roundtrip-doc-1', workspaceId)).toEqual([
1665
- expect.objectContaining({
1666
- taskId: 'task-cloud-link-roundtrip-1',
1667
- role: 'attachment',
1668
- deletedAt: null
1669
- })
1670
- ]);
1671
- expect(reloadedStore.listLinksForAsset('asset-cloud-link-roundtrip-file-1', workspaceId)).toEqual([
1672
- expect.objectContaining({
1673
- taskId: 'task-cloud-link-roundtrip-1',
1674
- role: 'attachment',
1675
- deletedAt: null
1676
- })
1677
- ]);
1678
- });
1679
- it('round-trips planning attachment links through pull and apply-local routes', async () => {
1680
- const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-cloud-');
1681
- const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-local-');
1682
- const cloudCore = new TaskforceCore({ projectRoot: cloudProjectRoot, storagePath: '.taskforce' });
1683
- seedAuthenticatedWorkspace(cloudCore);
1684
- cloudCore.createInitiative({
1685
- id: 'initiative-cloud-link-roundtrip-1',
1686
- title: 'Cloud initiative attachment target',
1687
- createdAt: '2026-03-18T09:00:00.000Z',
1688
- updatedAt: '2026-03-18T09:00:00.000Z'
1689
- }, workspaceId);
1690
- cloudCore.createWorkstream({
1691
- id: 'workstream-cloud-link-roundtrip-1',
1692
- initiativeId: 'initiative-cloud-link-roundtrip-1',
1693
- title: 'Cloud workstream attachment target',
1694
- createdAt: '2026-03-18T09:00:00.000Z',
1695
- updatedAt: '2026-03-18T09:00:00.000Z'
1696
- }, workspaceId);
1697
- const cloudBasePath = cloudCore.getPaths().basePath;
1698
- const cloudStore = new WorkspaceAssetStore(cloudCore.getDatabaseAdapter(), cloudCore.getTenantId());
1699
- const markdownPath = `workspaces/${workspaceId}/assets/documents/asset-cloud-planning-link-plan.md`;
1700
- const filePath = `workspaces/${workspaceId}/assets/files/asset-cloud-planning-link-notes.txt`;
1701
- fs.mkdirSync(path.dirname(path.join(cloudBasePath, markdownPath)), { recursive: true });
1702
- fs.mkdirSync(path.dirname(path.join(cloudBasePath, filePath)), { recursive: true });
1703
- fs.writeFileSync(path.join(cloudBasePath, markdownPath), '# planning sync\n', 'utf8');
1704
- fs.writeFileSync(path.join(cloudBasePath, filePath), 'planning attachment', 'utf8');
1705
- cloudStore.upsertAsset({
1706
- assetId: 'asset-cloud-planning-link-doc-1',
1707
- workspaceId,
1708
- kind: 'document',
1709
- originalFilename: 'plan.md',
1710
- mimeType: 'text/markdown',
1711
- storageProvider: 'local',
1712
- storageKey: markdownPath,
1713
- contentSha256: 'planning-doc-roundtrip-hash',
1714
- sizeBytes: '# planning sync\n'.length,
1715
- documentId: 'doc-cloud-planning-link-1',
1716
- version: 1,
1717
- isLatest: true,
1718
- createdAt: '2026-03-18T09:00:00.000Z',
1719
- updatedAt: '2026-03-18T09:00:00.000Z'
1720
- });
1721
- cloudStore.upsertLink({
1722
- workspaceId,
1723
- assetId: 'asset-cloud-planning-link-doc-1',
1724
- targetType: 'initiative',
1725
- targetId: 'initiative-cloud-link-roundtrip-1',
1726
- role: 'attachment',
1727
- updatedAt: '2026-03-18T10:00:00.000Z'
1728
- });
1729
- cloudStore.upsertAsset({
1730
- assetId: 'asset-cloud-planning-link-file-1',
1731
- workspaceId,
1732
- kind: 'file',
1733
- originalFilename: 'notes.txt',
1734
- mimeType: 'text/plain',
1735
- storageProvider: 'local',
1736
- storageKey: filePath,
1737
- contentSha256: 'planning-file-roundtrip-hash',
1738
- sizeBytes: 'planning attachment'.length,
1739
- isLatest: true,
1740
- createdAt: '2026-03-18T09:00:00.000Z',
1741
- updatedAt: '2026-03-18T09:00:00.000Z'
1742
- });
1743
- cloudStore.upsertLink({
1744
- workspaceId,
1745
- assetId: 'asset-cloud-planning-link-file-1',
1746
- targetType: 'workstream',
1747
- targetId: 'workstream-cloud-link-roundtrip-1',
1748
- role: 'attachment',
1749
- updatedAt: '2026-03-18T10:00:01.000Z'
1750
- });
1751
- const cloudRoutes = createRoutes(cloudCore, { authConfig });
1752
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', cloudRoutes);
1753
- expect(pullMatch).toBeTruthy();
1754
- const cookie = createAuthCookie(workspaceId);
1755
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=50&cursor=eyJ1IjoiMjAyNi0wMy0xOFQwOTozMDowMC4wMDBaIiwiaWQiOiJzZWVkIn0', undefined, { cookie });
1756
- const pullRes = createMockRes();
1757
- await pullMatch?.handler(pullReq, pullRes, {});
1758
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1759
- const pullBody = readJsonBody(pullRes);
1760
- const pulledChanges = Array.isArray(pullBody?.changes) ? pullBody.changes : [];
1761
- expect(pulledChanges).toEqual(expect.arrayContaining([
1762
- expect.objectContaining({
1763
- op: 'document-upsert',
1764
- path: markdownPath,
1765
- assetId: 'asset-cloud-planning-link-doc-1',
1766
- linkTargetType: 'initiative',
1767
- linkTargetId: 'initiative-cloud-link-roundtrip-1',
1768
- originalFilename: 'plan.md',
1769
- linkRole: 'attachment'
1770
- }),
1771
- expect.objectContaining({
1772
- op: 'asset-upsert',
1773
- path: filePath,
1774
- assetId: 'asset-cloud-planning-link-file-1',
1775
- linkTargetType: 'workstream',
1776
- linkTargetId: 'workstream-cloud-link-roundtrip-1',
1777
- originalFilename: 'notes.txt',
1778
- linkRole: 'attachment'
1779
- })
1780
- ]));
1781
- const localCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1782
- localCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1783
- localCore.createInitiative({
1784
- id: 'initiative-cloud-link-roundtrip-1',
1785
- title: 'Local initiative attachment target',
1786
- createdAt: '2026-03-18T09:00:00.000Z',
1787
- updatedAt: '2026-03-18T09:00:00.000Z'
1788
- }, workspaceId);
1789
- localCore.createWorkstream({
1790
- id: 'workstream-cloud-link-roundtrip-1',
1791
- initiativeId: 'initiative-cloud-link-roundtrip-1',
1792
- title: 'Local workstream attachment target',
1793
- createdAt: '2026-03-18T09:00:00.000Z',
1794
- updatedAt: '2026-03-18T09:00:00.000Z'
1795
- }, workspaceId);
1796
- const localRoutes = createRoutes(localCore, { authConfig: { runtimeMode: 'local', enabled: false } });
1797
- const applyMatch = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', localRoutes);
1798
- expect(applyMatch).toBeTruthy();
1799
- const applyReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: pulledChanges }, { 'x-taskforce-workspace-id': workspaceId });
1800
- const applyRes = createMockRes();
1801
- await applyMatch?.handler(applyReq, applyRes, {});
1802
- expect(applyRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1803
- const reloadedLocalCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1804
- const reloadedStore = new WorkspaceAssetStore(reloadedLocalCore.getDatabaseAdapter(), reloadedLocalCore.getTenantId());
1805
- expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, markdownPath), 'utf8')).toBe('# planning sync\n');
1806
- expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, filePath), 'utf8')).toBe('planning attachment');
1807
- expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-doc-1', workspaceId)).toEqual([
1808
- expect.objectContaining({
1809
- targetType: 'initiative',
1810
- targetId: 'initiative-cloud-link-roundtrip-1',
1811
- taskId: null,
1812
- role: 'attachment',
1813
- deletedAt: null
1814
- })
1815
- ]);
1816
- expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-file-1', workspaceId)).toEqual([
1817
- expect.objectContaining({
1818
- targetType: 'workstream',
1819
- targetId: 'workstream-cloud-link-roundtrip-1',
1820
- taskId: null,
1821
- role: 'attachment',
1822
- deletedAt: null
1823
- })
1824
- ]);
1825
- });
1826
- it('pushes document attachment links when only the link metadata changed locally', async () => {
1827
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-link-only-push-');
1828
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1829
- seedAuthenticatedWorkspace(seedCore);
1830
- seedCore.createTask({
1831
- id: 'task-doc-link-only-push-1',
1832
- title: 'Document link target',
1833
- status: 'task'
1834
- }, {
1835
- workspaceId,
1836
- allowClientProvidedId: true
1837
- });
1838
- const seedBasePath = seedCore.getPaths().basePath;
1839
- const relativeDocPath = `workspaces/${workspaceId}/assets/documents/asset-doc-link-only-push-plan.md`;
1840
- const absoluteDocPath = path.join(seedBasePath, relativeDocPath);
1841
- const content = '# already synced\n';
1842
- fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
1843
- fs.writeFileSync(absoluteDocPath, content, 'utf8');
1844
- const contentHash = createHash('sha256').update(content).digest('hex');
1845
- seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, contentHash, '2026-03-18T13:30:00.000Z');
1846
- const seedStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
1847
- seedStore.upsertAsset({
1848
- assetId: 'asset-doc-link-only-push-1',
1849
- workspaceId,
1850
- kind: 'document',
1851
- originalFilename: 'plan.md',
1852
- mimeType: 'text/markdown',
1853
- storageProvider: 'local',
1854
- storageKey: relativeDocPath,
1855
- contentSha256: contentHash,
1856
- sizeBytes: Buffer.byteLength(content, 'utf8'),
1857
- documentId: 'doc-link-only-push-1',
1858
- version: 1,
1859
- isLatest: true,
1860
- createdAt: '2026-03-18T13:30:00.000Z',
1861
- updatedAt: '2026-03-18T13:30:00.000Z'
1862
- });
1863
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1864
- const routes = createRoutes(core, { authConfig });
1865
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1866
- expect(pushMatch).toBeTruthy();
1867
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1868
- workspaceId,
1869
- idempotencyKey: 'doc-link-only-push-1',
1870
- changes: [
1871
- {
1872
- op: 'document-upsert',
1873
- path: relativeDocPath,
1874
- updatedAt: '2026-03-18T14:45:00.000Z',
1875
- content,
1876
- assetId: 'asset-doc-link-only-push-1',
1877
- documentId: 'doc-link-only-push-1',
1878
- referenceNumber: 12,
1879
- version: 1,
1880
- taskId: 'task-doc-link-only-push-1',
1881
- originalFilename: 'plan.md',
1882
- linkRole: 'attachment'
1883
- }
1884
- ]
1885
- }, { cookie: createAuthCookie(workspaceId) });
1886
- const res = createMockRes();
1887
- await pushMatch?.handler(req, res, {});
1888
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1889
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1890
- const reloadedStore = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1891
- expect(reloadedStore.listLinksForAsset('asset-doc-link-only-push-1', workspaceId)).toEqual([
1892
- expect.objectContaining({
1893
- taskId: 'task-doc-link-only-push-1',
1894
- role: 'attachment',
1895
- updatedAt: '2026-03-18T14:45:00.000Z',
1896
- deletedAt: null
1897
- })
1898
- ]);
1899
- });
1900
- it('propagates document-delete through authenticated push and omits deleted doc from pull payload', async () => {
1901
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-doc-delete-');
1902
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1903
- seedAuthenticatedWorkspace(seedCore);
1904
- const syncRoot = seedCore.getPaths().basePath;
1905
- const docsDir = path.join(syncRoot, 'docs');
1906
- fs.mkdirSync(docsDir, { recursive: true });
1907
- const relativeDocPath = 'docs/sync-delete-push.md';
1908
- const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1909
- fs.writeFileSync(absoluteDocPath, '# from local\n', 'utf8');
1910
- seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
1911
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1912
- const routes = createRoutes(core, { authConfig });
1913
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1914
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1915
- expect(pushMatch).toBeTruthy();
1916
- expect(pullMatch).toBeTruthy();
1917
- const cookie = createAuthCookie(workspaceId);
1918
- const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1919
- workspaceId,
1920
- idempotencyKey: 'doc-delete-integration-1',
1921
- changes: [
1922
- { op: 'document-delete', path: relativeDocPath }
1923
- ]
1924
- }, { cookie });
1925
- const pushRes = createMockRes();
1926
- await pushMatch?.handler(pushReq, pushRes, {});
1927
- expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1928
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1929
- expect(fs.existsSync(absoluteDocPath)).toBe(false);
1930
- expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
1931
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1932
- const pullRes = createMockRes();
1933
- await pullMatch?.handler(pullReq, pullRes, {});
1934
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1935
- const pullBody = readJsonBody(pullRes);
1936
- const documentUpserts = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1937
- .filter((change) => String(change?.op || '').toLowerCase() === 'document-upsert');
1938
- expect(documentUpserts.find((change) => change.path === relativeDocPath)).toBeUndefined();
1939
- });
1940
- it('round-trips task comments through authenticated push and pull payloads', async () => {
1941
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-comments-');
1942
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1943
- seedAuthenticatedWorkspace(seedCore);
1944
- const created = seedCore.createTask({
1945
- title: 'Comment sync task',
1946
- status: 'task'
1947
- }, { workspaceId });
1948
- seedCore.addComment(created.id, 'Synced activity comment', 'user', workspaceId);
1949
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1950
- const routes = createRoutes(core, { authConfig });
1951
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1952
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1953
- expect(pushMatch).toBeTruthy();
1954
- expect(pullMatch).toBeTruthy();
1955
- const cookie = createAuthCookie(workspaceId);
1956
- const currentTask = core.getTask(created.id, workspaceId);
1957
- expect(currentTask?.comments).toHaveLength(1);
1958
- expect(currentTask?.comments?.[0]?.submittedAuthor).toBe('user');
1959
- const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1960
- workspaceId,
1961
- idempotencyKey: 'comment-roundtrip-1',
1962
- changes: [
1963
- {
1964
- op: 'upsert',
1965
- archived: false,
1966
- task: currentTask
1967
- }
1968
- ]
1969
- }, { cookie });
1970
- const pushRes = createMockRes();
1971
- await pushMatch?.handler(pushReq, pushRes, {});
1972
- expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1973
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1974
- const pullRes = createMockRes();
1975
- await pullMatch?.handler(pullReq, pullRes, {});
1976
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1977
- const pullBody = readJsonBody(pullRes);
1978
- const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1979
- .find((change) => change?.op === 'upsert' && change?.task?.id === created.id);
1980
- expect(upsert?.task?.comments).toEqual([
1981
- expect.objectContaining({
1982
- id: expect.any(String),
1983
- author: 'user',
1984
- submittedAuthor: 'user',
1985
- text: 'Synced activity comment',
1986
- timestamp: expect.any(String)
1987
- })
1988
- ]);
1989
- });
1990
- it('omits local provisional reference fields from authenticated pull payloads', async () => {
1991
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-pull-task-ref-payload-');
1992
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1993
- seedAuthenticatedWorkspace(seedCore);
1994
- const created = seedCore.createTask({
1995
- title: 'Offline draft',
1996
- status: 'task'
1997
- }, {
1998
- workspaceId,
1999
- provisionalLocalReference: true
2000
- });
2001
- expect(created.localReferenceNumber).toBe(1);
2002
- expect(created.referenceLabel).toBe('LT-1');
2003
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2004
- const routes = createRoutes(core, { authConfig });
2005
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
2006
- expect(pullMatch).toBeTruthy();
2007
- const cookie = createAuthCookie(workspaceId);
2008
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
2009
- const pullRes = createMockRes();
2010
- await pullMatch?.handler(pullReq, pullRes, {});
2011
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2012
- const pullBody = readJsonBody(pullRes);
2013
- const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
2014
- .find((change) => change?.op === 'upsert' && change?.task?.id === created.id);
2015
- expect(upsert?.task).toEqual(expect.objectContaining({
2016
- id: created.id,
2017
- title: 'Offline draft',
2018
- referenceNumber: null
2019
- }));
2020
- expect(upsert?.task).not.toHaveProperty('localReferenceNumber');
2021
- expect(upsert?.task).not.toHaveProperty('referenceLabel');
2022
- });
2023
- it('returns a pushed provisional task on pull even when the caller cursor is newer than the stale local watermark', async () => {
2024
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-provisional-task-');
2025
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2026
- seedAuthenticatedWorkspace(seedCore);
2027
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2028
- const routes = createRoutes(core, { authConfig });
2029
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
2030
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
2031
- expect(pushMatch).toBeTruthy();
2032
- expect(pullMatch).toBeTruthy();
2033
- const cookie = createAuthCookie(workspaceId);
2034
- const taskId = 'task-provisional-roundtrip-1';
2035
- const staleUpdatedAt = '2026-04-11T21:29:04.121Z';
2036
- const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
2037
- workspaceId,
2038
- idempotencyKey: 'task-provisional-roundtrip-1',
2039
- changes: [
2040
- {
2041
- op: 'upsert',
2042
- archived: false,
2043
- task: {
2044
- id: taskId,
2045
- title: 'Offline provisional task',
2046
- status: 'task',
2047
- createdAt: '2026-04-11T21:28:52.410Z',
2048
- updatedAt: staleUpdatedAt,
2049
- referenceNumber: null
2050
- }
2051
- }
2052
- ]
2053
- }, { cookie });
2054
- const pushRes = createMockRes();
2055
- await pushMatch?.handler(pushReq, pushRes, {});
2056
- expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2057
- const pushBody = readJsonBody(pushRes);
2058
- expect(Array.isArray(pushBody?.appliedTaskUpserts)).toBe(true);
2059
- expect(pushBody?.appliedTaskUpserts).toEqual(expect.arrayContaining([
2060
- expect.objectContaining({
2061
- archived: false,
2062
- task: expect.objectContaining({
2063
- id: taskId,
2064
- title: 'Offline provisional task',
2065
- referenceNumber: 1
2066
- })
2067
- })
2068
- ]));
2069
- const persisted = core.getTask(taskId, workspaceId);
2070
- expect(persisted?.referenceNumber).toBe(1);
2071
- expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse(staleUpdatedAt));
2072
- const cursor = Buffer.from(JSON.stringify({
2073
- u: '2026-04-11T21:29:05.000Z',
2074
- id: 'seed'
2075
- })).toString('base64url');
2076
- const pullReq = createMockReq('GET', `/api/taskforce/sync/workspace/pull?workspaceId=${workspaceId}&limit=200&cursor=${cursor}`, undefined, { cookie });
2077
- const pullRes = createMockRes();
2078
- await pullMatch?.handler(pullReq, pullRes, {});
2079
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2080
- const pullBody = readJsonBody(pullRes);
2081
- const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
2082
- .find((change) => change?.op === 'upsert' && change?.task?.id === taskId);
2083
- expect(upsert?.task).toEqual(expect.objectContaining({
2084
- id: taskId,
2085
- title: 'Offline provisional task',
2086
- referenceNumber: 1
2087
- }));
2088
- });
2089
- it('keeps document deleted when push batch includes upsert and delete for same path', async () => {
2090
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-doc-delete-wins-');
2091
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2092
- seedAuthenticatedWorkspace(seedCore);
2093
- const syncRoot = seedCore.getPaths().basePath;
2094
- const docsDir = path.join(syncRoot, 'docs');
2095
- fs.mkdirSync(docsDir, { recursive: true });
2096
- const relativeDocPath = 'docs/sync-delete-wins.md';
2097
- const absoluteDocPath = path.join(syncRoot, relativeDocPath);
2098
- fs.writeFileSync(absoluteDocPath, '# old\n', 'utf8');
2099
- seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
2100
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2101
- const routes = createRoutes(core, { authConfig });
2102
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
2103
- expect(pushMatch).toBeTruthy();
2104
- const cookie = createAuthCookie(workspaceId);
2105
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
2106
- workspaceId,
2107
- idempotencyKey: 'doc-delete-integration-2',
2108
- changes: [
2109
- {
2110
- op: 'document-upsert',
2111
- path: relativeDocPath,
2112
- updatedAt: '2026-02-23T10:00:00.000Z',
2113
- content: '# stale upsert'
2114
- },
2115
- {
2116
- op: 'document-delete',
2117
- path: relativeDocPath
2118
- }
2119
- ]
2120
- }, { cookie });
2121
- const res = createMockRes();
2122
- await pushMatch?.handler(req, res, {});
2123
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2124
- expect(fs.existsSync(absoluteDocPath)).toBe(false);
2125
- expect(core.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
2126
- });
2127
- it('propagates binary asset deletes through authenticated push and persists canonical tombstones', async () => {
2128
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-asset-delete-');
2129
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2130
- seedAuthenticatedWorkspace(seedCore);
2131
- seedCore.createTask({
2132
- id: 'task-asset-delete-1',
2133
- title: 'Asset delete task',
2134
- status: 'task'
2135
- }, {
2136
- workspaceId,
2137
- allowClientProvidedId: true
2138
- });
2139
- const relativeAssetPath = 'workspaces/workspace-local-active/assets/files/asset-delete-auth-1-delete-me.bin';
2140
- const absoluteAssetPath = path.join(seedCore.getPaths().basePath, relativeAssetPath);
2141
- fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2142
- fs.writeFileSync(absoluteAssetPath, Buffer.from('delete-me'));
2143
- const seededStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
2144
- seededStore.upsertAsset({
2145
- assetId: 'asset-delete-auth-1',
2146
- workspaceId,
2147
- kind: 'file',
2148
- originalFilename: 'delete-me.bin',
2149
- mimeType: 'application/octet-stream',
2150
- storageProvider: 'local',
2151
- storageKey: relativeAssetPath,
2152
- contentSha256: 'seed-hash',
2153
- sizeBytes: 9,
2154
- createdAt: '2026-03-17T11:00:00.000Z',
2155
- updatedAt: '2026-03-17T11:00:00.000Z'
2156
- });
2157
- seededStore.upsertLink({
2158
- workspaceId,
2159
- assetId: 'asset-delete-auth-1',
2160
- taskId: 'task-asset-delete-1',
2161
- role: 'attachment',
2162
- updatedAt: '2026-03-17T11:00:00.000Z'
2163
- });
2164
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2165
- const routes = createRoutes(core, { authConfig });
2166
- const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
2167
- const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
2168
- expect(pushMatch).toBeTruthy();
2169
- expect(pullMatch).toBeTruthy();
2170
- const cookie = createAuthCookie(workspaceId);
2171
- const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
2172
- workspaceId,
2173
- idempotencyKey: 'asset-delete-integration-1',
2174
- changes: [
2175
- {
2176
- op: 'asset-delete',
2177
- path: relativeAssetPath,
2178
- assetId: 'asset-delete-auth-1'
2179
- }
2180
- ]
2181
- }, { cookie });
2182
- const pushRes = createMockRes();
2183
- await pushMatch?.handler(pushReq, pushRes, {});
2184
- expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2185
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2186
- const reloadedStore = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2187
- const deletedAsset = reloadedStore.get('asset-delete-auth-1', workspaceId);
2188
- const deletedLinks = reloadedStore.listLinksForAsset('asset-delete-auth-1', workspaceId, { includeDeleted: true });
2189
- expect(fs.existsSync(absoluteAssetPath)).toBe(false);
2190
- expect(deletedAsset?.deletedAt).toEqual(expect.any(String));
2191
- expect(deletedLinks).toEqual([
2192
- expect.objectContaining({
2193
- assetId: 'asset-delete-auth-1',
2194
- workspaceId,
2195
- taskId: 'task-asset-delete-1',
2196
- role: 'attachment',
2197
- deletedAt: expect.any(String)
2198
- })
2199
- ]);
2200
- const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
2201
- const pullRes = createMockRes();
2202
- await pullMatch?.handler(pullReq, pullRes, {});
2203
- expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2204
- const pullBody = readJsonBody(pullRes);
2205
- const assetUpserts = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
2206
- .filter((change) => String(change?.op || '').toLowerCase() === 'asset-upsert');
2207
- const assetDeletes = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
2208
- .filter((change) => String(change?.op || '').toLowerCase() === 'asset-delete');
2209
- expect(assetUpserts.find((change) => change.path === relativeAssetPath)).toBeUndefined();
2210
- expect(assetDeletes).toContainEqual(expect.objectContaining({
2211
- op: 'asset-delete',
2212
- path: relativeAssetPath,
2213
- assetId: 'asset-delete-auth-1'
2214
- }));
2215
- });
2216
- it('includes unattached annotated attachment sessions in the authenticated sync snapshot', async () => {
2217
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-annotated-snapshot-');
2218
- const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2219
- seedAuthenticatedWorkspace(seedCore);
2220
- const seededStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
2221
- seededStore.upsertAsset({
2222
- assetId: 'asset-image-unattached-1',
2223
- workspaceId,
2224
- kind: 'image',
2225
- originalFilename: 'unattached.png',
2226
- mimeType: 'image/png',
2227
- storageProvider: 'local',
2228
- storageKey: 'workspaces/workspace-local-active/assets/images/asset-image-unattached-1-unattached.png',
2229
- contentSha256: 'seed-hash-unattached',
2230
- sizeBytes: 42,
2231
- createdAt: '2026-03-30T20:00:00.000Z',
2232
- updatedAt: '2026-03-30T20:00:00.000Z'
2233
- });
2234
- seedCore.createAnnotatedAttachmentSession({
2235
- workspaceId,
2236
- baseImageAssetId: 'asset-image-unattached-1',
2237
- title: 'Loose image notes',
2238
- annotations: [
2239
- { id: 'annotation-1', kind: 'pin', order: 0, instruction: 'Check this', x: 0.2, y: 0.3 },
2240
- ],
2241
- });
2242
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2243
- const routes = createRoutes(core, { authConfig });
2244
- const snapshotMatch = matchRoute('GET', '/api/taskforce/sync/workspace/annotated-attachments', routes);
2245
- expect(snapshotMatch).toBeTruthy();
2246
- const cookie = createAuthCookie(workspaceId);
2247
- const req = createMockReq('GET', '/api/taskforce/sync/workspace/annotated-attachments?workspaceId=workspace-local-active', undefined, { cookie });
2248
- const res = createMockRes();
2249
- await snapshotMatch?.handler(req, res, {});
2250
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2251
- expect(readJsonBody(res).sessions).toEqual(expect.arrayContaining([
2252
- expect.objectContaining({
2253
- baseImageAssetId: 'asset-image-unattached-1',
2254
- taskId: '',
2255
- title: 'Loose image notes'
2256
- })
2257
- ]));
2258
- });
2259
- it('returns 401 on apply-local when auth is enabled but absent', async () => {
2260
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-auth-');
2261
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2262
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2263
- // Enable auth this time as we are testing it
2264
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: true } });
2265
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2266
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: [] }, { 'x-taskforce-workspace-id': workspaceId }
2267
- // No auth cookie or session
2268
- );
2269
- const res = createMockRes();
2270
- await match?.handler(req, res, {});
2271
- expect(res.writeHead).toHaveBeenCalledWith(401, expect.any(Object));
2272
- });
2273
- it('round-trips sync event details through the sync events API', async () => {
2274
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-events-details-');
2275
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2276
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2277
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2278
- const postMatch = matchRoute('POST', '/api/taskforce/sync/events', routes);
2279
- expect(postMatch).toBeTruthy();
2280
- const postReq = createMockReq('POST', '/api/taskforce/sync/events', {
2281
- workspaceId,
2282
- eventType: 'apply',
2283
- status: 'error',
2284
- errorMessage: 'Detected task reference number mismatch during normal sync.',
2285
- details: {
2286
- taskId: 'task-1',
2287
- existingReferenceNumber: 12,
2288
- incomingReferenceNumber: 18
2289
- }
2290
- });
2291
- const postRes = createMockRes();
2292
- await postMatch?.handler(postReq, postRes, {});
2293
- expect(postRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2294
- const getMatch = matchRoute('GET', '/api/taskforce/sync/events', routes);
2295
- expect(getMatch).toBeTruthy();
2296
- const getReq = createMockReq('GET', `/api/taskforce/sync/events?workspace_id=${workspaceId}&limit=5`);
2297
- const getRes = createMockRes();
2298
- await getMatch?.handler(getReq, getRes, {});
2299
- expect(getRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2300
- const payload = JSON.parse(String(getRes.end.mock.calls[0]?.[0] || '{}'));
2301
- expect(payload.events).toEqual(expect.arrayContaining([
2302
- expect.objectContaining({
2303
- eventType: 'apply',
2304
- status: 'error',
2305
- errorMessage: 'Detected task reference number mismatch during normal sync.',
2306
- details: expect.objectContaining({
2307
- taskId: 'task-1',
2308
- existingReferenceNumber: 12,
2309
- incomingReferenceNumber: 18
2310
- })
2311
- })
2312
- ]));
2313
- });
2314
- it('repairs orphaned startup document and asset metadata and exposes the recovered metadata through snapshots', async () => {
2315
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-recover-');
2316
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2317
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2318
- const basePath = core.getPaths().basePath;
2319
- const relativeDocPath = 'docs/recovered-startup-note.md';
2320
- const relativeAssetPath = `workspaces/${workspaceId}/assets/images/recovered-startup-image.png`;
2321
- const absoluteDocPath = path.join(basePath, relativeDocPath);
2322
- const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2323
- const assetBuffer = Buffer.from('recovered-image-binary');
2324
- fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
2325
- fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2326
- fs.writeFileSync(absoluteDocPath, '# repaired startup note\n', 'utf8');
2327
- fs.writeFileSync(absoluteAssetPath, assetBuffer);
2328
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2329
- const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2330
- const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2331
- const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2332
- expect(documentsMatch).toBeTruthy();
2333
- expect(assetsMatch).toBeTruthy();
2334
- expect(repairMatch).toBeTruthy();
2335
- const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2336
- const beforeDocumentsRes = createMockRes();
2337
- await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
2338
- expect(beforeDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2339
- const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
2340
- const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
2341
- .find((entry) => entry.path === relativeDocPath);
2342
- expect(beforeDoc).toBeUndefined();
2343
- const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2344
- const beforeAssetsRes = createMockRes();
2345
- await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2346
- expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2347
- const beforeAssetsBody = readJsonBody(beforeAssetsRes);
2348
- expect(beforeAssetsBody.assets).toEqual([]);
2349
- const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2350
- const repairRes = createMockRes();
2351
- await repairMatch?.handler(repairReq, repairRes, {});
2352
- expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2353
- expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2354
- success: true,
2355
- applied: 2,
2356
- skipped: 0,
2357
- failed: 0
2358
- }));
2359
- const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2360
- const afterDocumentsRes = createMockRes();
2361
- await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
2362
- expect(afterDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2363
- const afterDocumentsBody = readJsonBody(afterDocumentsRes);
2364
- const repairedDoc = (Array.isArray(afterDocumentsBody?.documents) ? afterDocumentsBody.documents : [])
2365
- .find((entry) => entry.path === relativeDocPath);
2366
- expect(repairedDoc).toEqual(expect.objectContaining({
2367
- path: relativeDocPath,
2368
- content: '# repaired startup note\n',
2369
- assetId: expect.stringMatching(/^asset-/),
2370
- documentId: expect.stringMatching(/^doc-/),
2371
- originalFilename: 'recovered-startup-note.md'
2372
- }));
2373
- const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2374
- const afterAssetsRes = createMockRes();
2375
- await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2376
- expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2377
- const afterAssetsBody = readJsonBody(afterAssetsRes);
2378
- const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
2379
- .find((entry) => entry.path === relativeAssetPath);
2380
- expect(repairedAsset).toEqual(expect.objectContaining({
2381
- path: relativeAssetPath,
2382
- assetId: expect.stringMatching(/^asset-/),
2383
- kind: 'image',
2384
- mimeType: 'image/png',
2385
- originalFilename: 'recovered-startup-image.png',
2386
- linkRole: 'image',
2387
- contentBase64: assetBuffer.toString('base64')
2388
- }));
2389
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2390
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2391
- expect(store.getByStorageKey(relativeDocPath, workspaceId)).toEqual(expect.objectContaining({
2392
- storageKey: relativeDocPath,
2393
- kind: 'document',
2394
- mimeType: 'text/markdown',
2395
- originalFilename: 'recovered-startup-note.md',
2396
- documentId: expect.stringMatching(/^doc-/)
2397
- }));
2398
- expect(store.getByStorageKey(relativeAssetPath, workspaceId)).toEqual(expect.objectContaining({
2399
- storageKey: relativeAssetPath,
2400
- kind: 'image',
2401
- mimeType: 'image/png',
2402
- originalFilename: 'recovered-startup-image.png'
2403
- }));
2404
- });
2405
- it('revives stale startup tombstones with the original metadata identifiers when local files are newer', async () => {
2406
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-revive-');
2407
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2408
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2409
- const basePath = core.getPaths().basePath;
2410
- const relativeDocPath = 'docs/revived-startup-note.md';
2411
- const relativeAssetPath = `workspaces/${workspaceId}/assets/images/revived-startup-image.png`;
2412
- const absoluteDocPath = path.join(basePath, relativeDocPath);
2413
- const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2414
- const revivedAt = new Date('2026-03-17T12:00:00.000Z');
2415
- const deletedAt = '2026-03-10T08:00:00.000Z';
2416
- const assetBuffer = Buffer.from('revived-image-binary');
2417
- fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
2418
- fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2419
- fs.writeFileSync(absoluteDocPath, '# revived startup note\n', 'utf8');
2420
- fs.writeFileSync(absoluteAssetPath, assetBuffer);
2421
- fs.utimesSync(absoluteDocPath, revivedAt, revivedAt);
2422
- fs.utimesSync(absoluteAssetPath, revivedAt, revivedAt);
2423
- const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
2424
- seededStore.upsertAsset({
2425
- assetId: 'asset-revived-doc-1',
2426
- workspaceId,
2427
- kind: 'document',
2428
- originalFilename: 'revived-startup-note.md',
2429
- mimeType: 'text/markdown',
2430
- storageProvider: 'local',
2431
- storageKey: relativeDocPath,
2432
- contentSha256: 'deleted-doc-hash',
2433
- sizeBytes: 1,
2434
- documentId: 'doc-revived-startup-1',
2435
- referenceNumber: 33,
2436
- version: 4,
2437
- createdAt: '2026-03-01T08:00:00.000Z',
2438
- updatedAt: deletedAt,
2439
- deletedAt,
2440
- purgeAfter: '2026-06-01T08:00:00.000Z'
2441
- });
2442
- seededStore.upsertAsset({
2443
- assetId: 'asset-revived-image-1',
2444
- workspaceId,
2445
- kind: 'image',
2446
- originalFilename: 'revived-startup-image.png',
2447
- mimeType: 'image/png',
2448
- storageProvider: 'local',
2449
- storageKey: relativeAssetPath,
2450
- contentSha256: 'deleted-image-hash',
2451
- sizeBytes: 1,
2452
- referenceNumber: 21,
2453
- createdAt: '2026-03-01T08:00:00.000Z',
2454
- updatedAt: deletedAt,
2455
- deletedAt,
2456
- purgeAfter: '2026-06-01T08:00:00.000Z'
2457
- });
2458
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2459
- const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2460
- const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2461
- const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2462
- expect(documentsMatch).toBeTruthy();
2463
- expect(assetsMatch).toBeTruthy();
2464
- expect(repairMatch).toBeTruthy();
2465
- const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2466
- const beforeDocumentsRes = createMockRes();
2467
- await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
2468
- const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
2469
- const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
2470
- .find((entry) => entry.path === relativeDocPath);
2471
- expect(beforeDoc).toBeUndefined();
2472
- const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2473
- const beforeAssetsRes = createMockRes();
2474
- await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2475
- expect(readJsonBody(beforeAssetsRes).assets).toEqual([]);
2476
- const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2477
- const repairRes = createMockRes();
2478
- await repairMatch?.handler(repairReq, repairRes, {});
2479
- expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2480
- success: true,
2481
- applied: 2,
2482
- skipped: 0,
2483
- failed: 0
2484
- }));
2485
- const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2486
- const afterDocumentsRes = createMockRes();
2487
- await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
2488
- const repairedDoc = (Array.isArray(readJsonBody(afterDocumentsRes)?.documents) ? readJsonBody(afterDocumentsRes).documents : [])
2489
- .find((entry) => entry.path === relativeDocPath);
2490
- expect(repairedDoc).toEqual(expect.objectContaining({
2491
- path: relativeDocPath,
2492
- assetId: 'asset-revived-doc-1',
2493
- documentId: 'doc-revived-startup-1',
2494
- referenceNumber: 33,
2495
- version: 5,
2496
- originalFilename: 'revived-startup-note.md'
2497
- }));
2498
- const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2499
- const afterAssetsRes = createMockRes();
2500
- await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2501
- const repairedAsset = (Array.isArray(readJsonBody(afterAssetsRes)?.assets) ? readJsonBody(afterAssetsRes).assets : [])
2502
- .find((entry) => entry.path === relativeAssetPath);
2503
- expect(repairedAsset).toEqual(expect.objectContaining({
2504
- path: relativeAssetPath,
2505
- assetId: 'asset-revived-image-1',
2506
- referenceNumber: 21,
2507
- kind: 'image',
2508
- mimeType: 'image/png',
2509
- originalFilename: 'revived-startup-image.png',
2510
- contentBase64: assetBuffer.toString('base64')
2511
- }));
2512
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2513
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2514
- expect(store.get('asset-revived-doc-1', workspaceId)).toEqual(expect.objectContaining({
2515
- assetId: 'asset-revived-doc-1',
2516
- documentId: 'doc-revived-startup-1',
2517
- referenceNumber: 33,
2518
- version: 5,
2519
- deletedAt: null
2520
- }));
2521
- expect(store.get('asset-revived-image-1', workspaceId)).toEqual(expect.objectContaining({
2522
- assetId: 'asset-revived-image-1',
2523
- referenceNumber: 21,
2524
- deletedAt: null
2525
- }));
2526
- });
2527
- it('removes stale startup asset links to missing tasks while preserving the asset snapshot', async () => {
2528
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-stale-link-');
2529
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2530
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2531
- const basePath = core.getPaths().basePath;
2532
- const relativeAssetPath = `workspaces/${workspaceId}/assets/files/stale-link-asset.txt`;
2533
- const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2534
- const assetBuffer = Buffer.from('stale-link-asset');
2535
- fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2536
- fs.writeFileSync(absoluteAssetPath, assetBuffer);
2537
- const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
2538
- seededStore.upsertAsset({
2539
- assetId: 'asset-stale-link-1',
2540
- workspaceId,
2541
- kind: 'file',
2542
- originalFilename: 'stale-link-asset.txt',
2543
- mimeType: 'text/plain',
2544
- storageProvider: 'local',
2545
- storageKey: relativeAssetPath,
2546
- contentSha256: createHash('sha256').update(assetBuffer).digest('hex'),
2547
- sizeBytes: assetBuffer.length,
2548
- createdAt: '2026-03-17T11:00:00.000Z',
2549
- updatedAt: '2026-03-17T11:00:00.000Z'
2550
- });
2551
- seededStore.upsertLink({
2552
- workspaceId,
2553
- assetId: 'asset-stale-link-1',
2554
- taskId: 'task-missing-link-1',
2555
- role: 'attachment',
2556
- updatedAt: '2026-03-17T11:00:00.000Z'
2557
- });
2558
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2559
- const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2560
- const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2561
- expect(assetsMatch).toBeTruthy();
2562
- expect(repairMatch).toBeTruthy();
2563
- const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2564
- const beforeAssetsRes = createMockRes();
2565
- await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2566
- expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2567
- const beforeAssetsBody = readJsonBody(beforeAssetsRes);
2568
- const beforeAsset = (Array.isArray(beforeAssetsBody?.assets) ? beforeAssetsBody.assets : [])
2569
- .find((entry) => entry.path === relativeAssetPath);
2570
- expect(beforeAsset).toEqual(expect.objectContaining({
2571
- path: relativeAssetPath,
2572
- assetId: 'asset-stale-link-1',
2573
- taskId: 'task-missing-link-1',
2574
- linkRole: 'attachment',
2575
- contentBase64: assetBuffer.toString('base64')
2576
- }));
2577
- const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2578
- const repairRes = createMockRes();
2579
- await repairMatch?.handler(repairReq, repairRes, {});
2580
- expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2581
- expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2582
- success: true,
2583
- applied: 1,
2584
- skipped: 0,
2585
- failed: 0
2586
- }));
2587
- const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2588
- const afterAssetsRes = createMockRes();
2589
- await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2590
- expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2591
- const afterAssetsBody = readJsonBody(afterAssetsRes);
2592
- const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
2593
- .find((entry) => entry.path === relativeAssetPath);
2594
- expect(repairedAsset).toEqual(expect.objectContaining({
2595
- path: relativeAssetPath,
2596
- assetId: 'asset-stale-link-1',
2597
- taskId: null,
2598
- linkRole: 'attachment',
2599
- contentBase64: assetBuffer.toString('base64')
2600
- }));
2601
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2602
- const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2603
- expect(store.get('asset-stale-link-1', workspaceId)).toEqual(expect.objectContaining({
2604
- assetId: 'asset-stale-link-1',
2605
- storageKey: relativeAssetPath,
2606
- deletedAt: null
2607
- }));
2608
- expect(store.listLinksForAsset('asset-stale-link-1', workspaceId)).toEqual([]);
2609
- expect(store.listLinksForAsset('asset-stale-link-1', workspaceId, { includeDeleted: true })).toEqual([
2610
- expect.objectContaining({
2611
- assetId: 'asset-stale-link-1',
2612
- taskId: 'task-missing-link-1',
2613
- role: 'attachment',
2614
- deletedAt: expect.any(String)
2615
- })
2616
- ]);
2617
- });
2618
- it('returns 400 on apply-local when workspaceId is null/empty/default', async () => {
2619
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-wsmissing-');
2620
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2621
- // Use false for auth to isolate workspaceId testing
2622
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2623
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2624
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: [] }
2625
- // Omit custom headers, and the body does not have workspaceId
2626
- );
2627
- const res = createMockRes();
2628
- await match?.handler(req, res, {});
2629
- expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
2630
- });
2631
- it('keeps repeated document scans correct and invalidates cache when file content changes', async () => {
2632
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-cache-');
2633
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2634
- const basePath = core.getPaths().basePath;
2635
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2636
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2637
- const uploadMatch = matchRoute('POST', '/api/taskforce/context-upload', routes);
2638
- const match = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2639
- expect(uploadMatch).toBeTruthy();
2640
- expect(match).toBeTruthy();
2641
- const uploadReq = createMockReq('POST', '/api/taskforce/context-upload', {
2642
- file: `data:text/markdown;base64,${Buffer.from('# cache test\n', 'utf8').toString('base64')}`,
2643
- originalName: 'cache-target.md',
2644
- workspaceId
2645
- }, { 'x-taskforce-workspace-id': workspaceId });
2646
- const uploadRes = createMockRes();
2647
- await uploadMatch?.handler(uploadReq, uploadRes, {});
2648
- expect(uploadRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
2649
- const uploadBody = JSON.parse(String(uploadRes.end.mock.calls.at(-1)?.[0] || '{}'));
2650
- const relativeDocPath = String(uploadBody?.fsPath || '');
2651
- const absoluteDocPath = path.join(basePath, relativeDocPath);
2652
- const req1 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2653
- const res1 = createMockRes();
2654
- await match?.handler(req1, res1, {});
2655
- expect(res1.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2656
- const body1 = JSON.parse(String(res1.end.mock.calls.at(-1)?.[0] || '{}'));
2657
- const req2 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2658
- const res2 = createMockRes();
2659
- await match?.handler(req2, res2, {});
2660
- expect(res2.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2661
- const body2 = JSON.parse(String(res2.end.mock.calls.at(-1)?.[0] || '{}'));
2662
- const firstDoc1 = (Array.isArray(body1?.documents) ? body1.documents : []).find((doc) => doc.path === relativeDocPath);
2663
- const firstDoc2 = (Array.isArray(body2?.documents) ? body2.documents : []).find((doc) => doc.path === relativeDocPath);
2664
- expect(firstDoc1?.content).toBe('# cache test\n');
2665
- expect(firstDoc2?.content).toBe('# cache test\n');
2666
- expect(firstDoc2?.updatedAt).toBe(firstDoc1?.updatedAt);
2667
- fs.writeFileSync(absoluteDocPath, '# cache test changed\n', 'utf8');
2668
- const req3 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2669
- const res3 = createMockRes();
2670
- await match?.handler(req3, res3, {});
2671
- expect(res3.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2672
- const body3 = JSON.parse(String(res3.end.mock.calls.at(-1)?.[0] || '{}'));
2673
- const firstDoc3 = (Array.isArray(body3?.documents) ? body3.documents : []).find((doc) => doc.path === relativeDocPath);
2674
- expect(firstDoc3?.content).toBe('# cache test changed\n');
2675
- });
2676
- it('includes locally uploaded canonical markdown attachments in the workspace document snapshot', async () => {
2677
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-canonical-doc-');
2678
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2679
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2680
- const task = core.createTask({ title: 'Task with markdown attachment' }, { workspaceId });
2681
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2682
- const uploadMatch = matchRoute('POST', '/api/taskforce/context-upload', routes);
2683
- expect(uploadMatch).toBeTruthy();
2684
- const uploadReq = createMockReq('POST', '/api/taskforce/context-upload', {
2685
- file: `data:text/markdown;base64,${Buffer.from('# local attachment\n', 'utf8').toString('base64')}`,
2686
- originalName: 'local-notes.md',
2687
- taskId: task.id
2688
- }, { 'x-taskforce-workspace-id': workspaceId });
2689
- const uploadRes = createMockRes();
2690
- await uploadMatch?.handler(uploadReq, uploadRes, {});
2691
- expect(uploadRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
2692
- const uploadBody = JSON.parse(String(uploadRes.end.mock.calls.at(-1)?.[0] || '{}'));
2693
- expect(String(uploadBody?.fsPath || '')).toContain(`workspaces/${workspaceId}/assets/documents/`);
2694
- const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2695
- expect(documentsMatch).toBeTruthy();
2696
- const req = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2697
- const res = createMockRes();
2698
- await documentsMatch?.handler(req, res, {});
2699
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2700
- const body = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
2701
- const relativeDocPath = String(uploadBody?.fsPath || '');
2702
- const doc = (Array.isArray(body?.documents) ? body.documents : []).find((entry) => entry.path === relativeDocPath);
2703
- expect(doc).toBeTruthy();
2704
- expect(doc.content).toBe('# local attachment\n');
2705
- });
2706
- it('does not sync raw docs directory markdown files because shared document sync must be canonical and workspace-scoped', async () => {
2707
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-exclude-raw-docs-');
2708
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2709
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2710
- const rawDocsDir = path.join(core.getPaths().basePath, 'docs');
2711
- fs.mkdirSync(rawDocsDir, { recursive: true });
2712
- fs.writeFileSync(path.join(rawDocsDir, 'legacy-shared.md'), '# legacy shared doc\n', 'utf8');
2713
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2714
- const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2715
- expect(documentsMatch).toBeTruthy();
2716
- const req = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2717
- const res = createMockRes();
2718
- await documentsMatch?.handler(req, res, {});
2719
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2720
- const body = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
2721
- const docs = Array.isArray(body?.documents) ? body.documents : [];
2722
- expect(docs.some((entry) => entry.path === 'docs/legacy-shared.md')).toBe(false);
2723
- });
2724
- it('converges task conflicts deterministically for equal timestamps regardless of apply order', async () => {
2725
- const applySequence = async (prefix, titlesInOrder) => {
2726
- const projectRoot = createIsolatedProjectRoot(prefix);
2727
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2728
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2729
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2730
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2731
- expect(match).toBeTruthy();
2732
- for (const title of titlesInOrder) {
2733
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
2734
- changes: [{
2735
- op: 'upsert',
2736
- archived: false,
2737
- task: {
2738
- id: 'task-concurrent-1',
2739
- title,
2740
- status: 'task',
2741
- createdAt: '2026-02-27T00:00:00.000Z',
2742
- updatedAt: '2026-02-27T00:00:00.000Z'
2743
- }
2744
- }]
2745
- }, { 'x-taskforce-workspace-id': workspaceId });
2746
- const res = createMockRes();
2747
- await match?.handler(req, res, {});
2748
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2749
- }
2750
- return core.getTask('task-concurrent-1', workspaceId)?.title || null;
2751
- };
2752
- const orderAB = await applySequence('taskforce-sync-conflict-task-ab-', ['alpha-title', 'zeta-title']);
2753
- const orderBA = await applySequence('taskforce-sync-conflict-task-ba-', ['zeta-title', 'alpha-title']);
2754
- expect(orderAB).toBeTruthy();
2755
- expect(orderBA).toBe(orderAB);
2756
- });
2757
- it('converges document metadata conflicts deterministically for equal timestamps regardless of apply order', async () => {
2758
- const applySequence = async (prefix, contentsInOrder) => {
2759
- const projectRoot = createIsolatedProjectRoot(prefix);
2760
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2761
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2762
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2763
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2764
- expect(match).toBeTruthy();
2765
- const relativeDocPath = 'docs/conflict-doc.md';
2766
- const absoluteDocPath = path.join(core.getPaths().basePath, relativeDocPath);
2767
- for (const content of contentsInOrder) {
2768
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
2769
- changes: [{
2770
- op: 'document-upsert',
2771
- path: relativeDocPath,
2772
- updatedAt: '2026-02-27T00:00:00.000Z',
2773
- content
2774
- }]
2775
- }, { 'x-taskforce-workspace-id': workspaceId });
2776
- const res = createMockRes();
2777
- await match?.handler(req, res, {});
2778
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2779
- }
2780
- return fs.existsSync(absoluteDocPath) ? fs.readFileSync(absoluteDocPath, 'utf8') : '';
2781
- };
2782
- const orderAB = await applySequence('taskforce-sync-conflict-doc-ab-', ['doc-content-alpha', 'doc-content-zeta']);
2783
- const orderBA = await applySequence('taskforce-sync-conflict-doc-ba-', ['doc-content-zeta', 'doc-content-alpha']);
2784
- expect(orderAB).toBeTruthy();
2785
- expect(orderBA).toBe(orderAB);
2786
- });
2787
- it('returns 200 with skipped count when updateTask throws during apply-local (regression for Bug 1)', async () => {
2788
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-updatethrow-');
2789
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2790
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2791
- // Create the task first so the apply-local path takes the updateTask branch
2792
- core.createTask({
2793
- id: 'task-update-throws',
2794
- title: 'Original title',
2795
- status: 'task'
2796
- }, {
2797
- workspaceId,
2798
- allowClientProvidedId: true
2799
- });
2800
- // Stub updateTask to throw on this specific task id
2801
- const originalUpdateTask = core.updateTask.bind(core);
2802
- vi.spyOn(core, 'updateTask').mockImplementationOnce((...args) => {
2803
- if (args[0] === 'task-update-throws') {
2804
- throw new Error('Rule violation: cannot update task');
2805
- }
2806
- return originalUpdateTask(...args);
2807
- });
2808
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2809
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2810
- expect(match).toBeTruthy();
2811
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
2812
- changes: [
2813
- {
2814
- op: 'upsert',
2815
- archived: false,
2816
- task: {
2817
- id: 'task-update-throws',
2818
- title: 'Updated title from cloud',
2819
- status: 'in-progress'
2820
- }
2821
- }
2822
- ]
2823
- }, { 'x-taskforce-workspace-id': workspaceId });
2824
- const res = createMockRes();
2825
- await match?.handler(req, res, {});
2826
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2827
- const body = readJsonBody(res);
2828
- expect(body.result.skipped).toBeGreaterThanOrEqual(1);
2829
- });
2830
- it('preserves seeded first-class taxonomy state when apply-local receives a partial taxonomy payload', async () => {
2831
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-taxonomy-partial-');
2832
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2833
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2834
- core.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
2835
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2836
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2837
- expect(match).toBeTruthy();
2838
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
2839
- changes: [
2840
- {
2841
- op: 'taxonomy-upsert',
2842
- updatedAt: '2026-03-30T17:05:00.000Z',
2843
- taxonomyState: {
2844
- categories: [{ value: 'default', label: 'General', icon: 'Inbox' }],
2845
- priorities: [
2846
- { value: 1, label: 'Low', color: 'teal-500', icon: 'ArrowDown' },
2847
- { value: 2, label: 'Medium', color: 'blue-500', icon: 'Minus' },
2848
- { value: 3, label: 'High', color: 'amber-500', icon: 'ArrowUp' }
2849
- ],
2850
- taxonomies: []
2851
- }
2852
- }
2853
- ]
2854
- }, { 'x-taskforce-workspace-id': workspaceId });
2855
- const res = createMockRes();
2856
- await match?.handler(req, res, {});
2857
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2858
- const state = core.getTaxonomyState(workspaceId);
2859
- expect((state.types || []).map((type) => type.value)).toEqual([
2860
- 'feature',
2861
- 'bug',
2862
- 'chore',
2863
- 'refactor',
2864
- 'documentation'
2865
- ]);
2866
- expect((state.priorities || []).map((priority) => priority.value)).toEqual([1, 2, 3, 4]);
2867
- });
2868
- it('preserves seeded first-class taxonomy state when bootstrap-push receives a partial taxonomy payload', async () => {
2869
- const projectRoot = createIsolatedProjectRoot('taskforce-sync-bootstrap-taxonomy-partial-');
2870
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2871
- seedAuthenticatedWorkspace(core);
2872
- core.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
2873
- const routes = createRoutes(core, { authConfig });
2874
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/bootstrap-push', routes);
2875
- expect(match).toBeTruthy();
2876
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/bootstrap-push', {
2877
- workspaceId,
2878
- tasks: [],
2879
- archived: [],
2880
- taxonomyState: {
2881
- categories: [{ value: 'default', label: 'General', icon: 'Inbox' }],
2882
- priorities: [
2883
- { value: 1, label: 'Low', color: 'teal-500', icon: 'ArrowDown' },
2884
- { value: 2, label: 'Medium', color: 'blue-500', icon: 'Minus' },
2885
- { value: 3, label: 'High', color: 'amber-500', icon: 'ArrowUp' }
2886
- ],
2887
- taxonomies: []
2888
- }
2889
- }, { cookie: createAuthCookie(workspaceId) });
2890
- const res = createMockRes();
2891
- await match?.handler(req, res, {});
2892
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2893
- const state = core.getTaxonomyState(workspaceId);
2894
- expect((state.types || []).map((type) => type.value)).toEqual([
2895
- 'feature',
2896
- 'bug',
2897
- 'chore',
2898
- 'refactor',
2899
- 'documentation'
2900
- ]);
2901
- expect((state.priorities || []).map((priority) => priority.value)).toEqual([1, 2, 3, 4]);
2902
- });
2903
- it('converges taxonomy conflicts deterministically for equal timestamps regardless of apply order', async () => {
2904
- const applySequence = async (prefix, taxonomiesInOrder) => {
2905
- const projectRoot = createIsolatedProjectRoot(prefix);
2906
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2907
- core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2908
- const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2909
- const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
2910
- expect(match).toBeTruthy();
2911
- for (const taxonomies of taxonomiesInOrder) {
2912
- const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
2913
- changes: [{
2914
- op: 'taxonomy-upsert',
2915
- updatedAt: '2026-02-27T00:00:00.000Z',
2916
- taxonomies
2917
- }]
2918
- }, { 'x-taskforce-workspace-id': workspaceId });
2919
- const res = createMockRes();
2920
- await match?.handler(req, res, {});
2921
- expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2922
- }
2923
- return core.getTaxonomies();
2924
- };
2925
- const alphaTaxonomies = [{ id: 'custom', label: 'Alpha', options: [], widgetType: 'select', isSystem: false, multiSelect: false }];
2926
- const zetaTaxonomies = [{ id: 'custom', label: 'Zeta', options: [], widgetType: 'select', isSystem: false, multiSelect: false }];
2927
- const orderAB = await applySequence('taskforce-sync-conflict-tax-ab-', [alphaTaxonomies, zetaTaxonomies]);
2928
- const orderBA = await applySequence('taskforce-sync-conflict-tax-ba-', [zetaTaxonomies, alphaTaxonomies]);
2929
- expect(Array.isArray(orderAB)).toBe(true);
2930
- expect(orderBA).toEqual(orderAB);
2931
- });
2932
- });