@taskforcehq/taskforce 0.3.311 → 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 (454) hide show
  1. package/README.md +40 -2
  2. package/dist/TaskforceCore.js +1 -1
  3. package/dist/components/features/InitiativesModule.js +1 -1
  4. package/dist/components/features/TaskSettings.js +1 -1
  5. package/dist/components/views/StandaloneLayout.js +3 -6
  6. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +1 -2
  7. package/dist/components/views/standalone/modals/AccountHubModal.js +2 -3
  8. package/dist/components/views/standalone/modals/HelpModal.js +15 -2
  9. package/dist/hooks/useFilterSort.js +1 -1
  10. package/dist/hooks/useTaskforce.js +2 -2
  11. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +5 -2
  12. package/dist/ui/assets/{AgentsModule-D8u9P5dZ.js → AgentsModule-BULqIo8e.js} +1 -1
  13. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CsUauFA2.js → AnnotatedAttachmentWorkspace-DHr4rHK9.js} +1 -1
  14. package/dist/ui/assets/{ContextAttachmentManager-DbQvDHFz.js → ContextAttachmentManager-DoMe2OpY.js} +1 -1
  15. package/dist/ui/assets/{DocumentWorkspace-BmTkN1A8.js → DocumentWorkspace-DumBWhvb.js} +1 -1
  16. package/dist/ui/assets/{EntityActivityTimeline-BXHTM-Nh.js → EntityActivityTimeline-sHk8Nzbq.js} +1 -1
  17. package/dist/ui/assets/{InitiativesModule-D33RyXx0.js → InitiativesModule-3X9UNzHh.js} +1 -1
  18. package/dist/ui/assets/{PlansPage-D0RXlT9_.js → PlansPage-C3A3EYaV.js} +1 -1
  19. package/dist/ui/assets/{TaskContextUpload-Vsg67lpR.js → TaskContextUpload-C8G7pWkX.js} +1 -1
  20. package/dist/ui/assets/{TaskSettings-DshLw7_d.js → TaskSettings-DM1o9Wla.js} +2 -2
  21. package/dist/ui/assets/{WorkflowsModule-CLM-1BnF.js → WorkflowsModule-CW0crzvD.js} +1 -1
  22. package/dist/ui/assets/documentReferences-BZB3nMRb.js +1 -0
  23. package/dist/ui/assets/index-BWFQ9mGn.js +5 -0
  24. package/dist/ui/assets/{index-DOR9PRqt.css → index-DcK_8tuE.css} +1 -1
  25. package/dist/ui/index.html +2 -2
  26. package/package.json +12 -4
  27. package/scripts/postinstall.js +4 -3
  28. package/dist/TaskforceCore.CategoryFallback.test.d.ts +0 -1
  29. package/dist/TaskforceCore.CategoryFallback.test.js +0 -101
  30. package/dist/TaskforceCore.filter-persistence.test.d.ts +0 -1
  31. package/dist/TaskforceCore.filter-persistence.test.js +0 -402
  32. package/dist/TaskforceCore.routing.test.d.ts +0 -1
  33. package/dist/TaskforceCore.routing.test.js +0 -311
  34. package/dist/TaskforceCore.test.d.ts +0 -1
  35. package/dist/TaskforceCore.test.js +0 -7910
  36. package/dist/TaskforceWidget.test.d.ts +0 -1
  37. package/dist/TaskforceWidget.test.js +0 -92
  38. package/dist/UnsavedChangesRelaxed.test.d.ts +0 -1
  39. package/dist/UnsavedChangesRelaxed.test.js +0 -16
  40. package/dist/__tests__/localizationCatalogs.test.d.ts +0 -1
  41. package/dist/__tests__/localizationCatalogs.test.js +0 -54
  42. package/dist/__tests__/workflowSimulation.test.d.ts +0 -1
  43. package/dist/__tests__/workflowSimulation.test.js +0 -113
  44. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts +0 -1
  45. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +0 -278
  46. package/dist/annotatedAttachments/payload.test.d.ts +0 -1
  47. package/dist/annotatedAttachments/payload.test.js +0 -67
  48. package/dist/compat/workspaceSyncCompat.test.d.ts +0 -1
  49. package/dist/compat/workspaceSyncCompat.test.js +0 -124
  50. package/dist/components/features/AgentsModule.test.d.ts +0 -1
  51. package/dist/components/features/AgentsModule.test.js +0 -601
  52. package/dist/components/features/AnnotatedAttachmentWorkspace.test.d.ts +0 -1
  53. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +0 -2883
  54. package/dist/components/features/DocumentWorkspace.test.d.ts +0 -1
  55. package/dist/components/features/DocumentWorkspace.test.js +0 -1439
  56. package/dist/components/features/TaskSettings.test.d.ts +0 -1
  57. package/dist/components/features/TaskSettings.test.js +0 -1674
  58. package/dist/components/features/settings/TaxonomyLibraryManager.test.d.ts +0 -1
  59. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +0 -141
  60. package/dist/components/task/TaskActionHeader.test.d.ts +0 -1
  61. package/dist/components/task/TaskActionHeader.test.js +0 -91
  62. package/dist/components/task/TaskCard.test.d.ts +0 -1
  63. package/dist/components/task/TaskCard.test.js +0 -513
  64. package/dist/components/task/TaskContextUpload.test.d.ts +0 -1
  65. package/dist/components/task/TaskContextUpload.test.js +0 -506
  66. package/dist/components/task/TaskForm.test.d.ts +0 -1
  67. package/dist/components/task/TaskForm.test.js +0 -946
  68. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +0 -1
  69. package/dist/components/task/TaskKanban.empty-column-drop.test.js +0 -66
  70. package/dist/components/task/TaskKanban.planning-drop.test.d.ts +0 -1
  71. package/dist/components/task/TaskKanban.planning-drop.test.js +0 -93
  72. package/dist/components/task/TaskKanban.test.d.ts +0 -1
  73. package/dist/components/task/TaskKanban.test.js +0 -99
  74. package/dist/components/task/TaskList.test.d.ts +0 -1
  75. package/dist/components/task/TaskList.test.js +0 -473
  76. package/dist/components/task/TaskStatusActions.test.d.ts +0 -1
  77. package/dist/components/task/TaskStatusActions.test.js +0 -54
  78. package/dist/components/task/taskKanbanDropRules.test.d.ts +0 -1
  79. package/dist/components/task/taskKanbanDropRules.test.js +0 -114
  80. package/dist/components/ui/LevelSelector.test.d.ts +0 -1
  81. package/dist/components/ui/LevelSelector.test.js +0 -55
  82. package/dist/components/ui/Markdown.test.d.ts +0 -1
  83. package/dist/components/ui/Markdown.test.js +0 -38
  84. package/dist/components/ui/MultiSelectFilter.test.d.ts +0 -1
  85. package/dist/components/ui/MultiSelectFilter.test.js +0 -74
  86. package/dist/components/ui/TaxonomyDropdown.test.d.ts +0 -1
  87. package/dist/components/ui/TaxonomyDropdown.test.js +0 -130
  88. package/dist/components/ui/TopNoticeLayer.test.d.ts +0 -1
  89. package/dist/components/ui/TopNoticeLayer.test.js +0 -25
  90. package/dist/components/views/AppShellHeader.test.d.ts +0 -1
  91. package/dist/components/views/AppShellHeader.test.js +0 -42
  92. package/dist/components/views/PlanComparisonPage.test.d.ts +0 -1
  93. package/dist/components/views/PlanComparisonPage.test.js +0 -758
  94. package/dist/components/views/PlansPage.test.d.ts +0 -1
  95. package/dist/components/views/PlansPage.test.js +0 -710
  96. package/dist/components/views/chrome/WorkspaceChromeSections.test.d.ts +0 -1
  97. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +0 -54
  98. package/dist/components/views/chrome/workspaceChrome.test.d.ts +0 -1
  99. package/dist/components/views/chrome/workspaceChrome.test.js +0 -52
  100. package/dist/components/views/panels/DocumentFilterToolbar.test.d.ts +0 -1
  101. package/dist/components/views/panels/DocumentFilterToolbar.test.js +0 -19
  102. package/dist/components/views/panels/FilterToolbar.test.d.ts +0 -1
  103. package/dist/components/views/panels/FilterToolbar.test.js +0 -47
  104. package/dist/components/views/panels/PlanningDrawer.test.d.ts +0 -1
  105. package/dist/components/views/panels/PlanningDrawer.test.js +0 -529
  106. package/dist/components/views/planningScope.test.d.ts +0 -1
  107. package/dist/components/views/planningScope.test.js +0 -60
  108. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +0 -1
  109. package/dist/components/views/standalone/modals/AccountHubModal.test.js +0 -72
  110. package/dist/components/views/standalone/modals/HelpModal.test.d.ts +0 -1
  111. package/dist/components/views/standalone/modals/HelpModal.test.js +0 -23
  112. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +0 -1
  113. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +0 -24
  114. package/dist/components/views/teamManagementAccess.test.d.ts +0 -1
  115. package/dist/components/views/teamManagementAccess.test.js +0 -44
  116. package/dist/config/deployment.test.d.ts +0 -1
  117. package/dist/config/deployment.test.js +0 -90
  118. package/dist/config/envSchema.test.d.ts +0 -1
  119. package/dist/config/envSchema.test.js +0 -125
  120. package/dist/core/AiProfileService.test.d.ts +0 -1
  121. package/dist/core/AiProfileService.test.js +0 -184
  122. package/dist/core/AiProfiles.test.d.ts +0 -1
  123. package/dist/core/AiProfiles.test.js +0 -1873
  124. package/dist/core/AuthTokenService.test.d.ts +0 -1
  125. package/dist/core/AuthTokenService.test.js +0 -78
  126. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +0 -1
  127. package/dist/core/BulkUpdateFieldsPreview.test.js +0 -62
  128. package/dist/core/CompletedAt.test.d.ts +0 -1
  129. package/dist/core/CompletedAt.test.js +0 -44
  130. package/dist/core/CreatedBy.test.d.ts +0 -1
  131. package/dist/core/CreatedBy.test.js +0 -188
  132. package/dist/core/DeletedTaskTrash.test.d.ts +0 -1
  133. package/dist/core/DeletedTaskTrash.test.js +0 -104
  134. package/dist/core/EntitlementsPolicy.test.d.ts +0 -1
  135. package/dist/core/EntitlementsPolicy.test.js +0 -280
  136. package/dist/core/GlobalSettingsService.test.d.ts +0 -1
  137. package/dist/core/GlobalSettingsService.test.js +0 -45
  138. package/dist/core/JsonBackupRuntime.test.d.ts +0 -1
  139. package/dist/core/JsonBackupRuntime.test.js +0 -155
  140. package/dist/core/PlanFeatureCatalog.test.d.ts +0 -1
  141. package/dist/core/PlanFeatureCatalog.test.js +0 -139
  142. package/dist/core/PlanVersionPolicy.test.d.ts +0 -1
  143. package/dist/core/PlanVersionPolicy.test.js +0 -236
  144. package/dist/core/PlanningEntities.test.d.ts +0 -1
  145. package/dist/core/PlanningEntities.test.js +0 -509
  146. package/dist/core/ScheduleFields.test.d.ts +0 -1
  147. package/dist/core/ScheduleFields.test.js +0 -193
  148. package/dist/core/SetupState.test.d.ts +0 -1
  149. package/dist/core/SetupState.test.js +0 -113
  150. package/dist/core/SignupMonetizationSettings.test.d.ts +0 -1
  151. package/dist/core/SignupMonetizationSettings.test.js +0 -515
  152. package/dist/core/Stats.test.d.ts +0 -1
  153. package/dist/core/Stats.test.js +0 -36
  154. package/dist/core/SystemAdmin.test.d.ts +0 -1
  155. package/dist/core/SystemAdmin.test.js +0 -1106
  156. package/dist/core/TaskActivitySync.test.d.ts +0 -1
  157. package/dist/core/TaskActivitySync.test.js +0 -90
  158. package/dist/core/TaskAttachmentsCanonical.test.d.ts +0 -1
  159. package/dist/core/TaskAttachmentsCanonical.test.js +0 -356
  160. package/dist/core/TaskAuditTimeline.test.d.ts +0 -1
  161. package/dist/core/TaskAuditTimeline.test.js +0 -319
  162. package/dist/core/TaskTaxonomyValidation.test.d.ts +0 -1
  163. package/dist/core/TaskTaxonomyValidation.test.js +0 -440
  164. package/dist/core/Taskforce.ids.test.d.ts +0 -1
  165. package/dist/core/Taskforce.ids.test.js +0 -611
  166. package/dist/core/Taskforce.listTasksSlim.test.d.ts +0 -1
  167. package/dist/core/Taskforce.listTasksSlim.test.js +0 -91
  168. package/dist/core/Taskforce.mcpAuth.test.d.ts +0 -1
  169. package/dist/core/Taskforce.mcpAuth.test.js +0 -450
  170. package/dist/core/Taskforce.realtimeMutation.test.d.ts +0 -1
  171. package/dist/core/Taskforce.realtimeMutation.test.js +0 -131
  172. package/dist/core/UserProfileAvatarDrafts.test.d.ts +0 -1
  173. package/dist/core/UserProfileAvatarDrafts.test.js +0 -138
  174. package/dist/core/WorkspaceDelete.test.d.ts +0 -1
  175. package/dist/core/WorkspaceDelete.test.js +0 -159
  176. package/dist/core/WorkspacePermissions.test.d.ts +0 -1
  177. package/dist/core/WorkspacePermissions.test.js +0 -270
  178. package/dist/core/WorkspacePlanMode.test.d.ts +0 -1
  179. package/dist/core/WorkspacePlanMode.test.js +0 -383
  180. package/dist/core/WorkspaceSyncSettings.test.d.ts +0 -1
  181. package/dist/core/WorkspaceSyncSettings.test.js +0 -65
  182. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +0 -1
  183. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +0 -212
  184. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +0 -1
  185. package/dist/documentReviews/DocumentReviewSessionStore.test.js +0 -127
  186. package/dist/hooks/authGuardPolicy.test.d.ts +0 -1
  187. package/dist/hooks/authGuardPolicy.test.js +0 -76
  188. package/dist/hooks/bootstrapTimeouts.test.d.ts +0 -1
  189. package/dist/hooks/bootstrapTimeouts.test.js +0 -12
  190. package/dist/hooks/sync/controlPlane.test.d.ts +0 -1
  191. package/dist/hooks/sync/controlPlane.test.js +0 -121
  192. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +0 -1
  193. package/dist/hooks/sync/lifecyclePolicy.test.js +0 -124
  194. package/dist/hooks/sync/orchestratorShared.test.d.ts +0 -1
  195. package/dist/hooks/sync/orchestratorShared.test.js +0 -98
  196. package/dist/hooks/sync/pullLifecycle.test.d.ts +0 -1
  197. package/dist/hooks/sync/pullLifecycle.test.js +0 -80
  198. package/dist/hooks/sync/recovery.test.d.ts +0 -1
  199. package/dist/hooks/sync/recovery.test.js +0 -292
  200. package/dist/hooks/sync/transfers.test.d.ts +0 -1
  201. package/dist/hooks/sync/transfers.test.js +0 -928
  202. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +0 -1
  203. package/dist/hooks/sync/useRecentSyncEvents.test.js +0 -124
  204. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +0 -1
  205. package/dist/hooks/sync/useSyncStatusActions.test.js +0 -133
  206. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +0 -1
  207. package/dist/hooks/sync/useSyncStatusControls.test.js +0 -88
  208. package/dist/hooks/useFilterSort.test.d.ts +0 -1
  209. package/dist/hooks/useFilterSort.test.js +0 -419
  210. package/dist/hooks/useRealtimeSync.test.d.ts +0 -1
  211. package/dist/hooks/useRealtimeSync.test.js +0 -311
  212. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +0 -1
  213. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +0 -54
  214. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +0 -1
  215. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +0 -228
  216. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +0 -1
  217. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +0 -2027
  218. package/dist/hooks/useTaskData.test.d.ts +0 -1
  219. package/dist/hooks/useTaskData.test.js +0 -291
  220. package/dist/hooks/useTaskMutations.test.d.ts +0 -1
  221. package/dist/hooks/useTaskMutations.test.js +0 -256
  222. package/dist/hooks/useTaskforce.contract.test.d.ts +0 -1
  223. package/dist/hooks/useTaskforce.contract.test.js +0 -7
  224. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +0 -1
  225. package/dist/hooks/useTaskforce.priority-filters.test.js +0 -28
  226. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +0 -1
  227. package/dist/hooks/useTaskforce.runtime-routing.test.js +0 -246
  228. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +0 -1
  229. package/dist/hooks/useTaskforce.sync-behavior.test.js +0 -3644
  230. package/dist/hooks/useWorkspaceSyncController.test.d.ts +0 -1
  231. package/dist/hooks/useWorkspaceSyncController.test.js +0 -192
  232. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +0 -1
  233. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +0 -77
  234. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +0 -1
  235. package/dist/hooks/workspace/workspaceLocalState.test.js +0 -59
  236. package/dist/localization/localizationCatalogs.test.d.ts +0 -1
  237. package/dist/localization/localizationCatalogs.test.js +0 -37
  238. package/dist/mcp/aiProfileBootstrap.test.d.ts +0 -1
  239. package/dist/mcp/aiProfileBootstrap.test.js +0 -68
  240. package/dist/mcp/canonicalAssetHelpers.test.d.ts +0 -1
  241. package/dist/mcp/canonicalAssetHelpers.test.js +0 -67
  242. package/dist/mcp/clientIntegrations.test.d.ts +0 -1
  243. package/dist/mcp/clientIntegrations.test.js +0 -219
  244. package/dist/mcp/optionResolution.test.d.ts +0 -1
  245. package/dist/mcp/optionResolution.test.js +0 -43
  246. package/dist/mcp/runtime.test.d.ts +0 -1
  247. package/dist/mcp/runtime.test.js +0 -5211
  248. package/dist/mcp/structuredCommentValidation.test.d.ts +0 -1
  249. package/dist/mcp/structuredCommentValidation.test.js +0 -31
  250. package/dist/mcp/structuredDocValidation.test.d.ts +0 -1
  251. package/dist/mcp/structuredDocValidation.test.js +0 -35
  252. package/dist/mcp/toolRegistry.test.d.ts +0 -1
  253. package/dist/mcp/toolRegistry.test.js +0 -34
  254. package/dist/migrations/billingSchemaParity.test.d.ts +0 -1
  255. package/dist/migrations/billingSchemaParity.test.js +0 -160
  256. package/dist/migrations/taskSchemaMigrations.test.d.ts +0 -1
  257. package/dist/migrations/taskSchemaMigrations.test.js +0 -225
  258. package/dist/qaOpen.test.d.ts +0 -1
  259. package/dist/qaOpen.test.js +0 -56
  260. package/dist/releaseVersion.test.d.ts +0 -1
  261. package/dist/releaseVersion.test.js +0 -32
  262. package/dist/runtime/appGates.test.d.ts +0 -1
  263. package/dist/runtime/appGates.test.js +0 -42
  264. package/dist/server/aiProfileAvatarAssets.test.d.ts +0 -1
  265. package/dist/server/aiProfileAvatarAssets.test.js +0 -213
  266. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +0 -1
  267. package/dist/server/annotatedAttachmentsRoutes.test.js +0 -532
  268. package/dist/server/auth.test.d.ts +0 -1
  269. package/dist/server/auth.test.js +0 -131
  270. package/dist/server/buildInfo.test.d.ts +0 -1
  271. package/dist/server/buildInfo.test.js +0 -29
  272. package/dist/server/cors.test.d.ts +0 -1
  273. package/dist/server/cors.test.js +0 -77
  274. package/dist/server/documentReviewRoutes.test.d.ts +0 -1
  275. package/dist/server/documentReviewRoutes.test.js +0 -190
  276. package/dist/server/email.test.d.ts +0 -1
  277. package/dist/server/email.test.js +0 -113
  278. package/dist/server/index.cookieProxy.test.d.ts +0 -1
  279. package/dist/server/index.cookieProxy.test.js +0 -67
  280. package/dist/server/index.rateLimit.test.d.ts +0 -1
  281. package/dist/server/index.rateLimit.test.js +0 -22
  282. package/dist/server/index.test.d.ts +0 -1
  283. package/dist/server/index.test.js +0 -283
  284. package/dist/server/rateLimit.test.d.ts +0 -1
  285. package/dist/server/rateLimit.test.js +0 -107
  286. package/dist/server/realtimeSync.auth.test.d.ts +0 -1
  287. package/dist/server/realtimeSync.auth.test.js +0 -62
  288. package/dist/server/realtimeSync.replay.test.d.ts +0 -1
  289. package/dist/server/realtimeSync.replay.test.js +0 -192
  290. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +0 -1
  291. package/dist/server/realtimeSync.workspaceIsolation.test.js +0 -101
  292. package/dist/server/routes/billing.test.d.ts +0 -1
  293. package/dist/server/routes/billing.test.js +0 -3515
  294. package/dist/server/routes/shared.test.d.ts +0 -1
  295. package/dist/server/routes/shared.test.js +0 -32
  296. package/dist/server/routes/sync.integration.test.d.ts +0 -1
  297. package/dist/server/routes/sync.integration.test.js +0 -2932
  298. package/dist/server/routes/syncAuxRoutes.test.d.ts +0 -1
  299. package/dist/server/routes/syncAuxRoutes.test.js +0 -158
  300. package/dist/server/routes/syncRouteGuards.test.d.ts +0 -1
  301. package/dist/server/routes/syncRouteGuards.test.js +0 -94
  302. package/dist/server/routes.test.d.ts +0 -1
  303. package/dist/server/routes.test.js +0 -15612
  304. package/dist/server/runtimeMode.test.d.ts +0 -1
  305. package/dist/server/runtimeMode.test.js +0 -26
  306. package/dist/server/securityHeaders.test.d.ts +0 -1
  307. package/dist/server/securityHeaders.test.js +0 -70
  308. package/dist/services/templateEngine.test.d.ts +0 -1
  309. package/dist/services/templateEngine.test.js +0 -150
  310. package/dist/services/templates.test.d.ts +0 -1
  311. package/dist/services/templates.test.js +0 -45
  312. package/dist/services/workflowDocumentFixtures.test.d.ts +0 -1
  313. package/dist/services/workflowDocumentFixtures.test.js +0 -78
  314. package/dist/services/workflowExportMatrix.test.d.ts +0 -1
  315. package/dist/services/workflowExportMatrix.test.js +0 -113
  316. package/dist/services/workflowExportSnapshots.test.d.ts +0 -1
  317. package/dist/services/workflowExportSnapshots.test.js +0 -30
  318. package/dist/services/workflowGeneration.test.d.ts +0 -1
  319. package/dist/services/workflowGeneration.test.js +0 -12
  320. package/dist/services/workflowSimulation.test.d.ts +0 -1
  321. package/dist/services/workflowSimulation.test.js +0 -113
  322. package/dist/services/workflowV2Policy.test.d.ts +0 -1
  323. package/dist/services/workflowV2Policy.test.js +0 -80
  324. package/dist/shared/customTaxonomyLibrary.test.d.ts +0 -1
  325. package/dist/shared/customTaxonomyLibrary.test.js +0 -20
  326. package/dist/shared/passwordPolicy.test.d.ts +0 -1
  327. package/dist/shared/passwordPolicy.test.js +0 -34
  328. package/dist/shared/runtimeContract.test.d.ts +0 -1
  329. package/dist/shared/runtimeContract.test.js +0 -23
  330. package/dist/shared/taxonomyLibrary.test.d.ts +0 -1
  331. package/dist/shared/taxonomyLibrary.test.js +0 -26
  332. package/dist/shared/taxonomyState.test.d.ts +0 -1
  333. package/dist/shared/taxonomyState.test.js +0 -71
  334. package/dist/soak/preflight.cli.d.ts +0 -1
  335. package/dist/soak/preflight.cli.js +0 -28
  336. package/dist/soak/preflight.d.ts +0 -26
  337. package/dist/soak/preflight.js +0 -113
  338. package/dist/soak/preflight.test.d.ts +0 -1
  339. package/dist/soak/preflight.test.js +0 -39
  340. package/dist/storage/objectStorageClient.test.d.ts +0 -1
  341. package/dist/storage/objectStorageClient.test.js +0 -82
  342. package/dist/storage/objectStorageConfig.test.d.ts +0 -1
  343. package/dist/storage/objectStorageConfig.test.js +0 -50
  344. package/dist/storage/postgresAdapter.test.d.ts +0 -1
  345. package/dist/storage/postgresAdapter.test.js +0 -29
  346. package/dist/storage/workspaceAssetStore.test.d.ts +0 -1
  347. package/dist/storage/workspaceAssetStore.test.js +0 -588
  348. package/dist/sync/cloudSyncApi.test.d.ts +0 -1
  349. package/dist/sync/cloudSyncApi.test.js +0 -111
  350. package/dist/sync/collaborationSyncPayload.test.d.ts +0 -1
  351. package/dist/sync/collaborationSyncPayload.test.js +0 -125
  352. package/dist/sync/contentSyncPayload.test.d.ts +0 -1
  353. package/dist/sync/contentSyncPayload.test.js +0 -143
  354. package/dist/sync/encryptionService.test.d.ts +0 -1
  355. package/dist/sync/encryptionService.test.js +0 -52
  356. package/dist/sync/planningSyncPayload.test.d.ts +0 -1
  357. package/dist/sync/planningSyncPayload.test.js +0 -92
  358. package/dist/sync/realtimeFeature.test.d.ts +0 -1
  359. package/dist/sync/realtimeFeature.test.js +0 -30
  360. package/dist/sync/realtimeReconcile.test.d.ts +0 -1
  361. package/dist/sync/realtimeReconcile.test.js +0 -43
  362. package/dist/sync/syncApplyHandlers.test.d.ts +0 -1
  363. package/dist/sync/syncApplyHandlers.test.js +0 -626
  364. package/dist/sync/syncService.test.d.ts +0 -1
  365. package/dist/sync/syncService.test.js +0 -903
  366. package/dist/sync/taskSyncPayload.test.d.ts +0 -1
  367. package/dist/sync/taskSyncPayload.test.js +0 -54
  368. package/dist/sync/workspacePullFeed.test.d.ts +0 -1
  369. package/dist/sync/workspacePullFeed.test.js +0 -438
  370. package/dist/sync/workspaceSyncModel.test.d.ts +0 -1
  371. package/dist/sync/workspaceSyncModel.test.js +0 -1442
  372. package/dist/test/setup.d.ts +0 -1
  373. package/dist/test/setup.js +0 -15
  374. package/dist/ui/assets/documentReferences-BPWp6ce0.js +0 -1
  375. package/dist/ui/assets/index-zvbo2Jse.js +0 -5
  376. package/dist/utils/accountProfileSummaryCache.test.d.ts +0 -1
  377. package/dist/utils/accountProfileSummaryCache.test.js +0 -63
  378. package/dist/utils/annotatedAttachments.test.d.ts +0 -1
  379. package/dist/utils/annotatedAttachments.test.js +0 -108
  380. package/dist/utils/assignees.test.d.ts +0 -1
  381. package/dist/utils/assignees.test.js +0 -25
  382. package/dist/utils/avatarUpload.test.d.ts +0 -1
  383. package/dist/utils/avatarUpload.test.js +0 -40
  384. package/dist/utils/canonicalAssetKind.test.d.ts +0 -1
  385. package/dist/utils/canonicalAssetKind.test.js +0 -24
  386. package/dist/utils/coalescedRefresh.test.d.ts +0 -1
  387. package/dist/utils/coalescedRefresh.test.js +0 -30
  388. package/dist/utils/constants.icons.test.d.ts +0 -1
  389. package/dist/utils/constants.icons.test.js +0 -41
  390. package/dist/utils/planParser.test.d.ts +0 -1
  391. package/dist/utils/planParser.test.js +0 -183
  392. package/dist/utils/planningReferences.test.d.ts +0 -1
  393. package/dist/utils/planningReferences.test.js +0 -17
  394. package/dist/utils/supportContact.test.d.ts +0 -1
  395. package/dist/utils/supportContact.test.js +0 -19
  396. package/dist/utils/syncEventDetails.test.d.ts +0 -1
  397. package/dist/utils/syncEventDetails.test.js +0 -23
  398. package/dist/utils/syncEventPresentation.test.d.ts +0 -1
  399. package/dist/utils/syncEventPresentation.test.js +0 -64
  400. package/dist/utils/syncStatusPresentation.test.d.ts +0 -1
  401. package/dist/utils/syncStatusPresentation.test.js +0 -164
  402. package/dist/utils/taskActivity.test.d.ts +0 -1
  403. package/dist/utils/taskActivity.test.js +0 -218
  404. package/dist/utils/taskDraftDefaults.test.d.ts +0 -1
  405. package/dist/utils/taskDraftDefaults.test.js +0 -34
  406. package/dist/utils/taskDragZones.test.d.ts +0 -1
  407. package/dist/utils/taskDragZones.test.js +0 -14
  408. package/dist/utils/taskNormalization.test.d.ts +0 -1
  409. package/dist/utils/taskNormalization.test.js +0 -158
  410. package/dist/utils/taskSearch.test.d.ts +0 -1
  411. package/dist/utils/taskSearch.test.js +0 -69
  412. package/dist/utils/taskUtils.test.d.ts +0 -1
  413. package/dist/utils/taskUtils.test.js +0 -19
  414. package/dist/utils/taskforceApiClient.test.d.ts +0 -1
  415. package/dist/utils/taskforceApiClient.test.js +0 -44
  416. package/dist/utils/taxonomyFormDefaults.test.d.ts +0 -1
  417. package/dist/utils/taxonomyFormDefaults.test.js +0 -63
  418. package/dist/utils/uiNotice.test.d.ts +0 -1
  419. package/dist/utils/uiNotice.test.js +0 -18
  420. package/dist/utils/workspaceIdentity.test.d.ts +0 -1
  421. package/dist/utils/workspaceIdentity.test.js +0 -61
  422. package/dist/utils/workspaceLifecycle.test.d.ts +0 -1
  423. package/dist/utils/workspaceLifecycle.test.js +0 -64
  424. package/dist/utils/workspaceSyncPresentation.test.d.ts +0 -1
  425. package/dist/utils/workspaceSyncPresentation.test.js +0 -71
  426. package/scripts/bootstrap-playwright-auth.mjs +0 -155
  427. package/scripts/check-cloud-mcp.mjs +0 -83
  428. package/scripts/check-compat-boundaries.mjs +0 -41
  429. package/scripts/check-env.ts +0 -31
  430. package/scripts/check-localization-strings.mjs +0 -49
  431. package/scripts/check-runtime-boundaries.mjs +0 -44
  432. package/scripts/debug-global.mjs +0 -34
  433. package/scripts/debug-login.mjs +0 -32
  434. package/scripts/debug-server.mjs +0 -36
  435. package/scripts/deploy-prod.sh +0 -41
  436. package/scripts/deploy-staging.sh +0 -23
  437. package/scripts/export-sqlite-to-postgres.mjs +0 -123
  438. package/scripts/generate-env-example.ts +0 -8
  439. package/scripts/mcp-stdio-adapter.mjs +0 -160
  440. package/scripts/migrate-task-data-assets.mjs +0 -408
  441. package/scripts/playwright-auth.sh +0 -19
  442. package/scripts/qa-open-lib.mjs +0 -175
  443. package/scripts/qa-open-lib.mjs.d.ts +0 -49
  444. package/scripts/qa-open.mjs +0 -352
  445. package/scripts/release-version-lib.mjs +0 -88
  446. package/scripts/release-version.mjs +0 -72
  447. package/scripts/run-billing-performance-smoke.sh +0 -24
  448. package/scripts/run-dev-proxy.sh +0 -51
  449. package/scripts/run-e2e-realtime.sh +0 -30
  450. package/scripts/run-smoke.sh +0 -35
  451. package/scripts/run-soak.sh +0 -41
  452. package/scripts/test-global.js +0 -143
  453. package/scripts/update-homebrew-formula.sh +0 -108
  454. 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
- });