@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -0,0 +1,377 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { applyCollaborationSyncChanges, applyContentSyncChanges, applyPreparedTaskSyncSnapshot, applyTaskSyncDeletes, classifyWorkspaceSyncChanges, prepareTaskSyncSnapshotApply, } from './syncApplyHandlers.js';
3
+ function createCoreFixture(overrides = {}) {
4
+ return {
5
+ resolveTaskIdentifier: vi.fn().mockReturnValue(null),
6
+ getTask: vi.fn().mockReturnValue(null),
7
+ deleteTaskForSync: vi.fn(),
8
+ applyWorkspaceSyncSnapshot: vi.fn().mockReturnValue({
9
+ total: 1,
10
+ created: 0,
11
+ updated: 1,
12
+ archived: 0,
13
+ unarchived: 0,
14
+ skipped: 0,
15
+ }),
16
+ notifyDocumentMetadataMutation: vi.fn(),
17
+ getDocumentWatermark: vi.fn().mockReturnValue(null),
18
+ getTenantId: vi.fn().mockReturnValue('tenant-1'),
19
+ getDocumentReviewSession: vi.fn().mockReturnValue(null),
20
+ upsertDocumentReviewSessionForSync: vi.fn(),
21
+ listDocumentReviewCommentsForSync: vi.fn().mockReturnValue([]),
22
+ upsertDocumentReviewCommentForSync: vi.fn(),
23
+ deleteDocumentReviewSessionForSync: vi.fn(),
24
+ deleteDocumentReviewCommentForSync: vi.fn(),
25
+ getAnnotatedAttachmentSession: vi.fn().mockReturnValue(null),
26
+ upsertAnnotatedAttachmentSessionForSync: vi.fn(),
27
+ deleteAnnotatedAttachmentSessionForSync: vi.fn(),
28
+ ...overrides,
29
+ };
30
+ }
31
+ describe('syncApplyHandlers', () => {
32
+ it('classifies task and workstream changes, then prepares and applies the task snapshot pipeline', () => {
33
+ const diagnostics = [];
34
+ const workspaceId = 'workspace-1';
35
+ const applyModel = classifyWorkspaceSyncChanges({
36
+ workspaceId,
37
+ incomingChanges: [
38
+ {
39
+ op: 'workstream-upsert',
40
+ workstream: {
41
+ id: 'workstream-1',
42
+ title: 'Delivery',
43
+ description: null,
44
+ createdAt: '2026-04-10T09:00:00.000Z',
45
+ updatedAt: '2026-04-10T09:00:00.000Z',
46
+ },
47
+ },
48
+ {
49
+ op: 'upsert',
50
+ archived: false,
51
+ task: {
52
+ id: 'task-1',
53
+ title: 'Superseded task',
54
+ status: 'task',
55
+ category: 'general',
56
+ type: 'feature',
57
+ createdAt: '2026-04-10T09:00:00.000Z',
58
+ updatedAt: '2026-04-10T10:00:00.000Z',
59
+ referenceNumber: 7,
60
+ },
61
+ },
62
+ {
63
+ op: 'upsert',
64
+ archived: true,
65
+ task: {
66
+ id: 'task-2',
67
+ title: 'Archived task',
68
+ status: 'done',
69
+ category: 'general',
70
+ type: 'feature',
71
+ createdAt: '2026-04-10T09:00:00.000Z',
72
+ updatedAt: '2026-04-10T10:30:00.000Z',
73
+ isArchived: true,
74
+ },
75
+ },
76
+ {
77
+ op: 'delete',
78
+ taskId: 'task-1',
79
+ },
80
+ {
81
+ op: 'delete',
82
+ taskId: 'task-stale',
83
+ },
84
+ ],
85
+ normalizeWorkspaceSyncDocumentPath: (rawPath) => typeof rawPath === 'string' ? rawPath : null,
86
+ normalizeTaskAssetPath: (rawPath) => typeof rawPath === 'string' ? rawPath : null,
87
+ normalizeIncomingAiProfileSurfaceType: () => null,
88
+ normalizeTaskEventSyncPayload: () => null,
89
+ recordSyncDiagnostic: (targetWorkspaceId, eventType, message, details) => {
90
+ diagnostics.push({ workspaceId: targetWorkspaceId, eventType, message, details });
91
+ },
92
+ });
93
+ expect(applyModel.planning.incomingWorkstreams.get('workstream-1')).toEqual(expect.objectContaining({
94
+ id: 'workstream-1',
95
+ title: 'Delivery',
96
+ }));
97
+ expect(applyModel.taskMutations.upserts).toHaveLength(2);
98
+ expect(applyModel.taskMutations.deletes.get('task-1')).toBe('2026-04-10T11:00:00.000Z');
99
+ const core = createCoreFixture({
100
+ resolveTaskIdentifier: vi.fn().mockReturnValue({
101
+ id: 'task-local-7',
102
+ title: 'Local incumbent',
103
+ referenceNumber: 7,
104
+ }),
105
+ getTask: vi.fn().mockImplementation((taskId) => {
106
+ if (taskId === 'task-1') {
107
+ return {
108
+ id: 'task-1',
109
+ title: 'Superseded task',
110
+ createdAt: '2026-04-10T09:00:00.000Z',
111
+ updatedAt: '2026-04-10T10:00:00.000Z',
112
+ isArchived: false,
113
+ };
114
+ }
115
+ if (taskId === 'task-stale') {
116
+ return {
117
+ id: 'task-stale',
118
+ title: 'Keep local task',
119
+ createdAt: '2026-04-10T09:00:00.000Z',
120
+ updatedAt: '2026-04-10T09:30:00.000Z',
121
+ isArchived: false,
122
+ };
123
+ }
124
+ return null;
125
+ }),
126
+ });
127
+ const prepared = prepareTaskSyncSnapshotApply({
128
+ core,
129
+ workspaceId,
130
+ taskMutations: applyModel.taskMutations,
131
+ recordSyncDiagnostic: (targetWorkspaceId, eventType, message, details) => {
132
+ diagnostics.push({ workspaceId: targetWorkspaceId, eventType, message, details });
133
+ },
134
+ });
135
+ expect(prepared.tasks).toEqual([]);
136
+ expect(prepared.archived).toEqual([
137
+ expect.objectContaining({
138
+ id: 'task-2',
139
+ title: 'Archived task',
140
+ }),
141
+ ]);
142
+ expect(diagnostics).toEqual(expect.arrayContaining([
143
+ expect.objectContaining({
144
+ workspaceId,
145
+ eventType: 'apply',
146
+ message: 'Detected task reference number mismatch during normal sync.',
147
+ details: expect.objectContaining({
148
+ referenceLabel: 'T-7',
149
+ existingTaskId: 'task-local-7',
150
+ taskId: 'task-1',
151
+ }),
152
+ }),
153
+ ]));
154
+ const deleteCount = applyTaskSyncDeletes({
155
+ core,
156
+ workspaceId,
157
+ taskMutations: applyModel.taskMutations,
158
+ recordSyncDiagnostic: vi.fn(),
159
+ });
160
+ expect(deleteCount).toBe(2);
161
+ expect(core.deleteTaskForSync).toHaveBeenCalledTimes(1);
162
+ expect(core.deleteTaskForSync).toHaveBeenCalledWith('task-1', workspaceId, { persistBackup: false });
163
+ const result = applyPreparedTaskSyncSnapshot({
164
+ core,
165
+ workspaceId,
166
+ prepared,
167
+ });
168
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
169
+ tasks: [],
170
+ archived: [
171
+ expect.objectContaining({
172
+ id: 'task-2',
173
+ title: 'Archived task',
174
+ isArchived: true,
175
+ }),
176
+ ],
177
+ }, workspaceId);
178
+ expect(result).toEqual(expect.objectContaining({ total: 1 }));
179
+ });
180
+ it('applies document and asset changes through the content handler helpers', async () => {
181
+ const core = createCoreFixture();
182
+ const writeWorkspaceSyncDocument = vi.fn().mockResolvedValue(undefined);
183
+ const writeWorkspaceSyncBinaryAsset = vi.fn().mockResolvedValue(undefined);
184
+ const deleteWorkspaceSyncDocument = vi.fn();
185
+ const deleteWorkspaceSyncBinaryAsset = vi.fn();
186
+ await applyContentSyncChanges({
187
+ core,
188
+ workspaceId: 'workspace-1',
189
+ incomingDocuments: [
190
+ {
191
+ path: 'docs/spec.md',
192
+ updatedAt: '2026-04-10T10:00:00.000Z',
193
+ content: '# Spec',
194
+ assetId: 'asset-doc-1',
195
+ },
196
+ ],
197
+ incomingDocumentDeletes: new Map([
198
+ ['docs/stale.md', '2026-04-10T11:00:00.000Z'],
199
+ ]),
200
+ incomingAssets: [
201
+ {
202
+ path: 'assets/proof.png',
203
+ updatedAt: '2026-04-10T10:05:00.000Z',
204
+ contentBase64: Buffer.from('png-bytes').toString('base64'),
205
+ assetId: 'asset-image-1',
206
+ kind: 'image',
207
+ mimeType: 'image/png',
208
+ },
209
+ ],
210
+ incomingAssetDeletes: new Map([
211
+ ['assets/stale.png', '2026-04-10T11:05:00.000Z'],
212
+ ]),
213
+ basePath: '/tmp/taskforce-sync-content',
214
+ hasObjectStorageClient: () => false,
215
+ hasDocumentSyncMetadataDrift: () => false,
216
+ hasBinaryAssetSyncMetadataDrift: () => false,
217
+ getExistingDocument: () => null,
218
+ writeWorkspaceSyncDocument,
219
+ deleteWorkspaceSyncDocument,
220
+ writeWorkspaceSyncBinaryAsset,
221
+ deleteWorkspaceSyncBinaryAsset,
222
+ getExistingBinaryAsset: () => null,
223
+ recordSyncDiagnostic: vi.fn(),
224
+ });
225
+ expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md', assetId: 'asset-doc-1' }), 'workspace-1', { allowReferenceNumberReassignment: true });
226
+ expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png', assetId: 'asset-image-1' }), 'workspace-1', { allowReferenceNumberReassignment: true });
227
+ expect(deleteWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', 'docs/stale.md', 'workspace-1');
228
+ expect(deleteWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', 'assets/stale.png', 'workspace-1');
229
+ expect(core.notifyDocumentMetadataMutation).toHaveBeenNthCalledWith(1, 'workspace-1', ['docs/spec.md', 'assets/proof.png'], 'upsert');
230
+ expect(core.notifyDocumentMetadataMutation).toHaveBeenNthCalledWith(2, 'workspace-1', ['docs/stale.md', 'assets/stale.png'], 'delete');
231
+ });
232
+ it('only allows reference reassignment for content writes when the content changed or repair is explicit', async () => {
233
+ const core = createCoreFixture({
234
+ getDocumentWatermark: vi.fn().mockReturnValue(null),
235
+ });
236
+ const writeWorkspaceSyncDocument = vi.fn().mockResolvedValue(undefined);
237
+ const writeWorkspaceSyncBinaryAsset = vi.fn().mockResolvedValue(undefined);
238
+ await applyContentSyncChanges({
239
+ core,
240
+ workspaceId: 'workspace-1',
241
+ incomingDocuments: [
242
+ {
243
+ path: 'docs/spec.md',
244
+ updatedAt: '2026-04-10T10:00:00.000Z',
245
+ content: '# Spec',
246
+ assetId: 'asset-doc-1',
247
+ referenceNumber: 22,
248
+ },
249
+ ],
250
+ incomingDocumentDeletes: new Map(),
251
+ incomingAssets: [
252
+ {
253
+ path: 'assets/proof.png',
254
+ updatedAt: '2026-04-10T10:05:00.000Z',
255
+ contentBase64: Buffer.from('new-bytes').toString('base64'),
256
+ assetId: 'asset-image-1',
257
+ kind: 'image',
258
+ mimeType: 'image/png',
259
+ referenceNumber: 33,
260
+ },
261
+ ],
262
+ incomingAssetDeletes: new Map(),
263
+ basePath: '/tmp/taskforce-sync-content',
264
+ hasObjectStorageClient: () => false,
265
+ hasDocumentSyncMetadataDrift: () => true,
266
+ hasBinaryAssetSyncMetadataDrift: () => true,
267
+ getExistingDocument: () => ({
268
+ updatedAt: '2026-04-10T10:00:00.000Z',
269
+ contentHash: '6bb50a17e2dc2fc14caea95a15cd8572018b6d22d74fb7b9ad015b5a0a8a935e',
270
+ }),
271
+ writeWorkspaceSyncDocument,
272
+ deleteWorkspaceSyncDocument: vi.fn(),
273
+ writeWorkspaceSyncBinaryAsset,
274
+ deleteWorkspaceSyncBinaryAsset: vi.fn(),
275
+ getExistingBinaryAsset: () => ({
276
+ updatedAt: '2026-04-10T10:00:00.000Z',
277
+ contentSha256: 'old-hash',
278
+ }),
279
+ recordSyncDiagnostic: vi.fn(),
280
+ });
281
+ expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md' }), 'workspace-1', { allowReferenceNumberReassignment: false });
282
+ expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png' }), 'workspace-1', { allowReferenceNumberReassignment: true });
283
+ });
284
+ it('applies collaboration upserts and deletes through the collaboration handlers', () => {
285
+ const core = createCoreFixture();
286
+ applyCollaborationSyncChanges({
287
+ core,
288
+ workspaceId: 'workspace-1',
289
+ incomingDocumentReviewSessions: new Map([
290
+ ['session-1', {
291
+ id: 'session-1',
292
+ assetId: 'asset-doc-1',
293
+ documentId: 'doc-1',
294
+ documentVersion: 3,
295
+ title: 'Review round',
296
+ status: 'open',
297
+ comments: [],
298
+ createdByActorId: 'user-1',
299
+ updatedByActorId: 'user-1',
300
+ createdAt: '2026-04-10T09:00:00.000Z',
301
+ updatedAt: '2026-04-10T10:00:00.000Z',
302
+ deletedAt: null,
303
+ }],
304
+ ]),
305
+ incomingDocumentReviewSessionDeletes: new Map([
306
+ ['session-2', '2026-04-10T11:00:00.000Z'],
307
+ ]),
308
+ incomingDocumentReviewComments: new Map([
309
+ ['comment-1', {
310
+ id: 'comment-1',
311
+ sessionId: 'session-1',
312
+ body: 'Looks good',
313
+ anchor: { kind: 'document' },
314
+ order: 0,
315
+ authorActorId: 'user-1',
316
+ createdAt: '2026-04-10T09:05:00.000Z',
317
+ updatedAt: '2026-04-10T10:05:00.000Z',
318
+ deletedAt: null,
319
+ }],
320
+ ]),
321
+ incomingDocumentReviewCommentDeletes: new Map([
322
+ ['comment-2', '2026-04-10T11:05:00.000Z'],
323
+ ]),
324
+ incomingAnnotatedAttachmentSessions: new Map([
325
+ ['annotation-session-1', {
326
+ id: 'annotation-session-1',
327
+ workspaceId: 'workspace-1',
328
+ taskId: 'task-1',
329
+ baseImageAssetId: 'asset-image-1',
330
+ title: 'Annotations',
331
+ globalInstruction: 'Inspect issue',
332
+ annotations: [],
333
+ createdByActorId: 'user-1',
334
+ updatedByActorId: 'user-1',
335
+ createdAt: '2026-04-10T09:10:00.000Z',
336
+ updatedAt: '2026-04-10T10:10:00.000Z',
337
+ deletedAt: null,
338
+ }],
339
+ ]),
340
+ incomingAnnotatedAttachmentSessionDeletes: new Map([
341
+ ['annotation-session-2', '2026-04-10T11:10:00.000Z'],
342
+ ]),
343
+ recordSyncDiagnostic: vi.fn(),
344
+ createSyncRouteError: (message, details) => Object.assign(new Error(message), { details }),
345
+ });
346
+ expect(core.upsertDocumentReviewSessionForSync).toHaveBeenCalledWith(expect.objectContaining({
347
+ id: 'session-1',
348
+ workspaceId: 'workspace-1',
349
+ assetId: 'asset-doc-1',
350
+ }));
351
+ expect(core.deleteDocumentReviewSessionForSync).toHaveBeenCalledWith({
352
+ workspaceId: 'workspace-1',
353
+ sessionId: 'session-2',
354
+ deletedAt: '2026-04-10T11:00:00.000Z',
355
+ });
356
+ expect(core.upsertDocumentReviewCommentForSync).toHaveBeenCalledWith(expect.objectContaining({
357
+ id: 'comment-1',
358
+ sessionId: 'session-1',
359
+ workspaceId: 'workspace-1',
360
+ }));
361
+ expect(core.deleteDocumentReviewCommentForSync).toHaveBeenCalledWith({
362
+ workspaceId: 'workspace-1',
363
+ commentId: 'comment-2',
364
+ deletedAt: '2026-04-10T11:05:00.000Z',
365
+ });
366
+ expect(core.upsertAnnotatedAttachmentSessionForSync).toHaveBeenCalledWith(expect.objectContaining({
367
+ id: 'annotation-session-1',
368
+ workspaceId: 'workspace-1',
369
+ baseImageAssetId: 'asset-image-1',
370
+ }));
371
+ expect(core.deleteAnnotatedAttachmentSessionForSync).toHaveBeenCalledWith({
372
+ workspaceId: 'workspace-1',
373
+ sessionId: 'annotation-session-2',
374
+ deletedAt: '2026-04-10T11:10:00.000Z',
375
+ });
376
+ });
377
+ });
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSyncPayload } from './workspaceSyncModel';
1
+ import type { WorkspaceSyncPayload, WorkspaceSyncTaskSnapshot } from './workspaceSyncModel';
2
2
  export declare function isTransientWorkspaceSyncStatus(status: number | null | undefined): boolean;
3
3
  export interface EnsureWorkspaceReadyInput {
4
4
  cloudAuthConfigured: boolean;
@@ -77,10 +77,16 @@ export interface WorkspacePushSyncResult {
77
77
  currentAnnotatedAttachmentSessionIds: Set<string>;
78
78
  deleteAnnotatedAttachmentSessionIds: Set<string>;
79
79
  currentAnnotatedAttachmentSessionWatermarks: Map<string, string>;
80
+ currentTaskEventWatermarks: Map<string, string>;
80
81
  /** Watermarks for every task upsert that was successfully pushed. */
81
82
  pushedWatermarks: Map<string, string>;
82
83
  /** Server-emitted realtime mutation event ids for this push apply. */
83
84
  emittedEventIds: string[];
85
+ /** Authoritative task snapshots returned immediately by the push route for incoming task upserts. */
86
+ appliedTaskUpserts: Array<{
87
+ archived: boolean;
88
+ task: WorkspaceSyncTaskSnapshot;
89
+ }>;
84
90
  }
85
91
  export declare function runWorkspacePushSyncService(input: WorkspacePushSyncInput): Promise<WorkspacePushSyncResult>;
86
92
  export type WorkspacePullFailureKind = 'pull_http' | 'apply_auth' | 'apply_all_candidates' | 'apply_payload' | 'exception';
@@ -113,8 +119,10 @@ export interface WorkspacePullSyncResult {
113
119
  pulledDeleteTaskIds: Set<string>;
114
120
  pulledActiveUpserts: boolean;
115
121
  pulledArchivedUpserts: boolean;
122
+ pulledTaskEventUpserts?: boolean;
116
123
  documentDecryptFailures?: string[];
117
124
  emittedEventIds?: string[];
125
+ serverDiagnostics?: Record<string, unknown> | null;
118
126
  }
119
127
  export declare function runWorkspacePullSyncService(input: WorkspacePullSyncInput): Promise<WorkspacePullSyncResult>;
120
128
  export declare function syncUserGlobalSettingsService(input: UserSettingsSyncInput): Promise<UserSettingsSyncResult>;
@@ -187,8 +187,10 @@ export async function runWorkspacePushSyncService(input) {
187
187
  currentAnnotatedAttachmentSessionIds: input.payload.currentAnnotatedAttachmentSessionIds,
188
188
  deleteAnnotatedAttachmentSessionIds: input.payload.deleteAnnotatedAttachmentSessionIds,
189
189
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
190
+ currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
190
191
  pushedWatermarks: new Map(),
191
- emittedEventIds: []
192
+ emittedEventIds: [],
193
+ appliedTaskUpserts: []
192
194
  };
193
195
  }
194
196
  const pushData = asObjectRecord(pushResult.data);
@@ -197,6 +199,23 @@ export async function runWorkspacePushSyncService(input) {
197
199
  .map((entry) => String(entry || '').trim())
198
200
  .filter(Boolean)))
199
201
  : [];
202
+ const appliedTaskUpserts = Array.isArray(pushData.appliedTaskUpserts)
203
+ ? pushData.appliedTaskUpserts
204
+ .map((entry) => {
205
+ const source = entry && typeof entry === 'object' ? entry : {};
206
+ const task = source.task && typeof source.task === 'object'
207
+ ? source.task
208
+ : null;
209
+ const taskId = String(task?.id || '').trim();
210
+ if (!task || !taskId)
211
+ return null;
212
+ return {
213
+ archived: source.archived === true,
214
+ task
215
+ };
216
+ })
217
+ .filter((entry) => Boolean(entry))
218
+ : [];
200
219
  return {
201
220
  success: true,
202
221
  syncedAt: new Date().toISOString(),
@@ -226,8 +245,10 @@ export async function runWorkspacePushSyncService(input) {
226
245
  currentAnnotatedAttachmentSessionIds: input.payload.currentAnnotatedAttachmentSessionIds,
227
246
  deleteAnnotatedAttachmentSessionIds: input.payload.deleteAnnotatedAttachmentSessionIds,
228
247
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
248
+ currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
229
249
  pushedWatermarks: input.payload.pushedWatermarks,
230
- emittedEventIds
250
+ emittedEventIds,
251
+ appliedTaskUpserts
231
252
  };
232
253
  }
233
254
  export async function runWorkspacePullSyncService(input) {
@@ -240,9 +261,11 @@ export async function runWorkspacePullSyncService(input) {
240
261
  const pulledDeleteTaskIds = new Set();
241
262
  let pulledActiveUpserts = false;
242
263
  let pulledArchivedUpserts = false;
264
+ let pulledTaskEventUpserts = false;
243
265
  const documentDecryptFailures = new Set();
244
266
  const emittedEventIds = new Set();
245
267
  const pulledAnnotatedAttachmentSessionIds = new Set();
268
+ let lastServerDiagnostics = null;
246
269
  const maxPages = Number.isFinite(Number(input.maxPages)) ? Math.max(1, Math.floor(Number(input.maxPages))) : 20;
247
270
  try {
248
271
  while (hasMore && pages < maxPages) {
@@ -279,11 +302,17 @@ export async function runWorkspacePullSyncService(input) {
279
302
  pulledDeleteTaskIds,
280
303
  pulledActiveUpserts,
281
304
  pulledArchivedUpserts,
282
- documentDecryptFailures: Array.from(documentDecryptFailures)
305
+ pulledTaskEventUpserts,
306
+ documentDecryptFailures: Array.from(documentDecryptFailures),
307
+ serverDiagnostics: null
283
308
  };
284
309
  }
285
310
  const pullData = pullRes.data || {};
286
311
  const changes = Array.isArray(pullData?.changes) ? pullData.changes : [];
312
+ const serverDiagnostics = pullData?.diagnostics && typeof pullData.diagnostics === 'object'
313
+ ? pullData.diagnostics
314
+ : null;
315
+ lastServerDiagnostics = serverDiagnostics;
287
316
  const hasWorkspaceMembersSnapshot = Array.isArray(pullData?.workspaceMembers);
288
317
  const workspaceMembers = hasWorkspaceMembersSnapshot
289
318
  ? pullData.workspaceMembers
@@ -303,6 +332,10 @@ export async function runWorkspacePullSyncService(input) {
303
332
  pulledAnnotatedAttachmentSessionIds.add(sessionId);
304
333
  continue;
305
334
  }
335
+ if (change.op === 'task-event-upsert') {
336
+ pulledTaskEventUpserts = true;
337
+ continue;
338
+ }
306
339
  if (change.op === 'document-upsert') {
307
340
  if (typeof change.content === 'string' && typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
308
341
  try {
@@ -365,7 +398,9 @@ export async function runWorkspacePullSyncService(input) {
365
398
  pulledDeleteTaskIds,
366
399
  pulledActiveUpserts,
367
400
  pulledArchivedUpserts,
368
- documentDecryptFailures: Array.from(documentDecryptFailures)
401
+ pulledTaskEventUpserts,
402
+ documentDecryptFailures: Array.from(documentDecryptFailures),
403
+ serverDiagnostics
369
404
  };
370
405
  }
371
406
  const hasTransientApplyFailure = applyFailures.some((entry) => {
@@ -391,8 +426,10 @@ export async function runWorkspacePullSyncService(input) {
391
426
  pulledDeleteTaskIds,
392
427
  pulledActiveUpserts,
393
428
  pulledArchivedUpserts,
429
+ pulledTaskEventUpserts,
394
430
  documentDecryptFailures: Array.from(documentDecryptFailures),
395
- emittedEventIds: Array.from(emittedEventIds)
431
+ emittedEventIds: Array.from(emittedEventIds),
432
+ serverDiagnostics: lastServerDiagnostics
396
433
  };
397
434
  }
398
435
  const applyPayloadData = asObjectRecord(applyResult.data);
@@ -411,8 +448,10 @@ export async function runWorkspacePullSyncService(input) {
411
448
  pulledDeleteTaskIds,
412
449
  pulledActiveUpserts,
413
450
  pulledArchivedUpserts,
451
+ pulledTaskEventUpserts,
414
452
  documentDecryptFailures: Array.from(documentDecryptFailures),
415
- emittedEventIds: Array.from(emittedEventIds)
453
+ emittedEventIds: Array.from(emittedEventIds),
454
+ serverDiagnostics: lastServerDiagnostics
416
455
  };
417
456
  }
418
457
  applyMs += (Date.now() - applyStartedAtMs);
@@ -460,8 +499,10 @@ export async function runWorkspacePullSyncService(input) {
460
499
  pulledDeleteTaskIds,
461
500
  pulledActiveUpserts,
462
501
  pulledArchivedUpserts,
502
+ pulledTaskEventUpserts,
463
503
  documentDecryptFailures: Array.from(documentDecryptFailures),
464
- emittedEventIds: Array.from(emittedEventIds)
504
+ emittedEventIds: Array.from(emittedEventIds),
505
+ serverDiagnostics: lastServerDiagnostics
465
506
  };
466
507
  }
467
508
  const hasTransientApplyFailure = bootstrapApplyFailures.some((entry) => {
@@ -487,8 +528,10 @@ export async function runWorkspacePullSyncService(input) {
487
528
  pulledDeleteTaskIds,
488
529
  pulledActiveUpserts,
489
530
  pulledArchivedUpserts,
531
+ pulledTaskEventUpserts,
490
532
  documentDecryptFailures: Array.from(documentDecryptFailures),
491
- emittedEventIds: Array.from(emittedEventIds)
533
+ emittedEventIds: Array.from(emittedEventIds),
534
+ serverDiagnostics: lastServerDiagnostics
492
535
  };
493
536
  }
494
537
  applyMs += (Date.now() - bootstrapApplyStartedAtMs);
@@ -504,8 +547,10 @@ export async function runWorkspacePullSyncService(input) {
504
547
  pulledDeleteTaskIds,
505
548
  pulledActiveUpserts,
506
549
  pulledArchivedUpserts,
550
+ pulledTaskEventUpserts,
507
551
  documentDecryptFailures: Array.from(documentDecryptFailures),
508
- emittedEventIds: Array.from(emittedEventIds)
552
+ emittedEventIds: Array.from(emittedEventIds),
553
+ serverDiagnostics: lastServerDiagnostics
509
554
  };
510
555
  }
511
556
  catch (error) {
@@ -523,8 +568,10 @@ export async function runWorkspacePullSyncService(input) {
523
568
  pulledDeleteTaskIds,
524
569
  pulledActiveUpserts,
525
570
  pulledArchivedUpserts,
571
+ pulledTaskEventUpserts,
526
572
  documentDecryptFailures: Array.from(documentDecryptFailures),
527
- emittedEventIds: Array.from(emittedEventIds)
573
+ emittedEventIds: Array.from(emittedEventIds),
574
+ serverDiagnostics: null
528
575
  };
529
576
  }
530
577
  }
@@ -37,6 +37,7 @@ function buildPayload(overrides = {}) {
37
37
  currentAnnotatedAttachmentSessionIds: new Set(),
38
38
  deleteAnnotatedAttachmentSessionIds: new Set(),
39
39
  currentAnnotatedAttachmentSessionWatermarks: new Map(),
40
+ currentTaskEventWatermarks: new Map(),
40
41
  pushedWatermarks: new Map(),
41
42
  ...overrides
42
43
  };
@@ -500,6 +501,53 @@ describe('syncService', () => {
500
501
  delete process.env.TASKFORCE_SYNC_KEY;
501
502
  }
502
503
  });
504
+ it('tracks event-only task pulls so callers can refresh visible task history', async () => {
505
+ vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
506
+ ok: true,
507
+ status: 200,
508
+ data: {
509
+ success: true,
510
+ changes: [{
511
+ op: 'task-event-upsert',
512
+ event: {
513
+ id: 'task-event-1',
514
+ taskId: 'task-1',
515
+ workspaceId: 'ws-1',
516
+ action: 'task-status-changed',
517
+ actor: 'ai-profile-1',
518
+ actorType: 'ai',
519
+ createdAt: '2026-04-13T10:05:00.000Z',
520
+ details: {
521
+ changes: {
522
+ status: {
523
+ from: 'in-progress',
524
+ to: 'done'
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }],
530
+ nextCursor: null,
531
+ hasMore: false
532
+ }
533
+ });
534
+ vi.mocked(applyLocalWorkspaceChanges).mockResolvedValueOnce({
535
+ ok: true,
536
+ failures: [],
537
+ appliedCount: 1,
538
+ data: { success: true }
539
+ });
540
+ const res = await runWorkspacePullSyncService({
541
+ resolveCloudAuthUrl: mockResolveCloudAuthUrl,
542
+ workspaceId: 'ws-1',
543
+ cursor: null,
544
+ bootstrap: false
545
+ });
546
+ expect(res.success).toBe(true);
547
+ expect(res.pulledTaskEventUpserts).toBe(true);
548
+ expect(res.pulledActiveUpserts).toBe(false);
549
+ expect(res.pulledArchivedUpserts).toBe(false);
550
+ });
503
551
  it('zero-change bootstrap pull (no changes, hasMore false) returns success true', async () => {
504
552
  vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
505
553
  ok: true,
@@ -15,6 +15,17 @@ export interface WorkspacePullFeedResult {
15
15
  changes: EncodedWorkspaceSyncChange[];
16
16
  nextCursor: string | null;
17
17
  hasMore: boolean;
18
+ diagnostics: {
19
+ totalEntries: number;
20
+ pageEntries: number;
21
+ skippedHydrationEntries: number;
22
+ documentIndexCount: number;
23
+ assetIndexCount: number;
24
+ indexMs: number;
25
+ hydrationMs: number;
26
+ encodeMs: number;
27
+ buildMs: number;
28
+ };
18
29
  }
19
30
  export declare function buildWorkspacePullFeed(input: {
20
31
  core: SyncCore;