@taskforcehq/taskforce 0.3.303 → 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.
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +12 -10
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -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,6 +119,7 @@ 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[];
|
|
118
125
|
serverDiagnostics?: Record<string, unknown> | null;
|
package/dist/sync/syncService.js
CHANGED
|
@@ -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,6 +261,7 @@ 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();
|
|
@@ -280,6 +302,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
280
302
|
pulledDeleteTaskIds,
|
|
281
303
|
pulledActiveUpserts,
|
|
282
304
|
pulledArchivedUpserts,
|
|
305
|
+
pulledTaskEventUpserts,
|
|
283
306
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
284
307
|
serverDiagnostics: null
|
|
285
308
|
};
|
|
@@ -309,6 +332,10 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
309
332
|
pulledAnnotatedAttachmentSessionIds.add(sessionId);
|
|
310
333
|
continue;
|
|
311
334
|
}
|
|
335
|
+
if (change.op === 'task-event-upsert') {
|
|
336
|
+
pulledTaskEventUpserts = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
312
339
|
if (change.op === 'document-upsert') {
|
|
313
340
|
if (typeof change.content === 'string' && typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
|
|
314
341
|
try {
|
|
@@ -371,6 +398,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
371
398
|
pulledDeleteTaskIds,
|
|
372
399
|
pulledActiveUpserts,
|
|
373
400
|
pulledArchivedUpserts,
|
|
401
|
+
pulledTaskEventUpserts,
|
|
374
402
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
375
403
|
serverDiagnostics
|
|
376
404
|
};
|
|
@@ -398,6 +426,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
398
426
|
pulledDeleteTaskIds,
|
|
399
427
|
pulledActiveUpserts,
|
|
400
428
|
pulledArchivedUpserts,
|
|
429
|
+
pulledTaskEventUpserts,
|
|
401
430
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
402
431
|
emittedEventIds: Array.from(emittedEventIds),
|
|
403
432
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -419,6 +448,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
419
448
|
pulledDeleteTaskIds,
|
|
420
449
|
pulledActiveUpserts,
|
|
421
450
|
pulledArchivedUpserts,
|
|
451
|
+
pulledTaskEventUpserts,
|
|
422
452
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
423
453
|
emittedEventIds: Array.from(emittedEventIds),
|
|
424
454
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -469,6 +499,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
469
499
|
pulledDeleteTaskIds,
|
|
470
500
|
pulledActiveUpserts,
|
|
471
501
|
pulledArchivedUpserts,
|
|
502
|
+
pulledTaskEventUpserts,
|
|
472
503
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
473
504
|
emittedEventIds: Array.from(emittedEventIds),
|
|
474
505
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -497,6 +528,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
497
528
|
pulledDeleteTaskIds,
|
|
498
529
|
pulledActiveUpserts,
|
|
499
530
|
pulledArchivedUpserts,
|
|
531
|
+
pulledTaskEventUpserts,
|
|
500
532
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
501
533
|
emittedEventIds: Array.from(emittedEventIds),
|
|
502
534
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -515,6 +547,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
515
547
|
pulledDeleteTaskIds,
|
|
516
548
|
pulledActiveUpserts,
|
|
517
549
|
pulledArchivedUpserts,
|
|
550
|
+
pulledTaskEventUpserts,
|
|
518
551
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
519
552
|
emittedEventIds: Array.from(emittedEventIds),
|
|
520
553
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -535,6 +568,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
535
568
|
pulledDeleteTaskIds,
|
|
536
569
|
pulledActiveUpserts,
|
|
537
570
|
pulledArchivedUpserts,
|
|
571
|
+
pulledTaskEventUpserts,
|
|
538
572
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
539
573
|
emittedEventIds: Array.from(emittedEventIds),
|
|
540
574
|
serverDiagnostics: null
|
|
@@ -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,
|