@taskforcehq/taskforce 0.3.301 → 0.3.303
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 +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -24,9 +24,11 @@
|
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
26
|
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
27
|
+
import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
|
|
27
28
|
import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
|
|
28
29
|
import { parsePlan } from '../../utils/planParser.js';
|
|
29
30
|
import { getCanonicalDocumentName, getReadableDocumentName, isGeneratedDocumentName } from '../../utils/documentNames.js';
|
|
31
|
+
import { requestRuntimeMode } from './shared.js';
|
|
30
32
|
import { parseJsonBody } from '../routes.js';
|
|
31
33
|
export function registerDocumentRoutes(deps) {
|
|
32
34
|
const { addRoute, core, context, requestWorkspaceId, ensureAdminRole, resolveEffectiveObjectStorageConfig, getObjectStorageClient, resolveStorageWorkspaceId, ensureContextUploadAllowed, parseStorageFolderSegments, buildCanonicalWorkspaceAssetRelativePath, inferAssetKindFromPath, appendStorageTelemetry, scanAndRecordDocument, buildTaskAttachmentPayload, workspaceAssetStore, taskAssetStore, normalizeDocumentPathInput, getAssetMetadata, getCanonicalAssetMetadata, documentRetentionDays, appendDocumentPurgeHistory, markAssetDeleted, isAssetDeleted, clearAssetDeleted, ensureWithinDir, sanitizeDownloadFilename, contentTypeForExt, inferInlineDocumentFromPath, resolveCanonicalDocumentRead, inferWorkspaceIdFromStoragePath, inferTaskIdFromStoragePath, } = deps;
|
|
@@ -1097,7 +1099,10 @@ export function registerDocumentRoutes(deps) {
|
|
|
1097
1099
|
type: node.type || 'feature',
|
|
1098
1100
|
priority: typeof node.priority === 'number' ? node.priority : 2,
|
|
1099
1101
|
status: 'task',
|
|
1100
|
-
}, {
|
|
1102
|
+
}, {
|
|
1103
|
+
workspaceId,
|
|
1104
|
+
provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
|
|
1105
|
+
});
|
|
1101
1106
|
created.push(taskItem.id);
|
|
1102
1107
|
totalCreated++;
|
|
1103
1108
|
}
|
|
@@ -30,6 +30,6 @@ export declare function makeResolveAdminRequestActor(core: TaskforceCore, contex
|
|
|
30
30
|
systemRole: "system_admin" | "user";
|
|
31
31
|
};
|
|
32
32
|
export declare function makeAuditAdminAction(core: TaskforceCore, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
33
|
-
export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
33
|
+
export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
|
|
34
34
|
export declare function makeRequireCloudRuntime(context: RouteContext): (res: ServerResponse, feature: string) => boolean;
|
|
35
35
|
export declare function makeEnsureCloudPlanData(context: RouteContext): (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
@@ -167,9 +167,11 @@ export function makeEnsureAppAccess(core, context) {
|
|
|
167
167
|
return true;
|
|
168
168
|
const code = accountAccess.gate === 'plan_selection_required'
|
|
169
169
|
? 'PLAN_SELECTION_REQUIRED'
|
|
170
|
-
: accountAccess.gate === '
|
|
171
|
-
? '
|
|
172
|
-
: '
|
|
170
|
+
: accountAccess.gate === 'checkout_pending'
|
|
171
|
+
? 'CHECKOUT_PENDING'
|
|
172
|
+
: accountAccess.gate === 'misconfigured'
|
|
173
|
+
? 'ACCOUNT_ACCESS_MISCONFIGURED'
|
|
174
|
+
: 'ACCOUNT_ENTITLEMENT_REQUIRED';
|
|
173
175
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
174
176
|
res.end(JSON.stringify({
|
|
175
177
|
error: accountAccess.message || 'This account cannot access the app.',
|
|
@@ -257,13 +259,21 @@ export function makeAuditAdminAction(core, context) {
|
|
|
257
259
|
console.info(`[Taskforce Admin Audit] ${JSON.stringify(payload)}`);
|
|
258
260
|
};
|
|
259
261
|
}
|
|
260
|
-
export function makeAuditAuthAction(core, requestWorkspaceId) {
|
|
262
|
+
export function makeAuditAuthAction(core, requestWorkspaceId, context) {
|
|
261
263
|
return function auditAuthAction(req, action, details) {
|
|
264
|
+
const authConfig = context.authConfig;
|
|
265
|
+
const resolvedAccess = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
|
|
266
|
+
const actorUserId = resolvedAccess?.authenticated && resolvedAccess.userId && resolvedAccess.userId !== 'anonymous'
|
|
267
|
+
? resolvedAccess.userId
|
|
268
|
+
: requestUserId(req);
|
|
269
|
+
const actorRole = resolvedAccess?.authenticated
|
|
270
|
+
? String(resolvedAccess.role || '').trim().toLowerCase() || requestRole(req)
|
|
271
|
+
: requestRole(req);
|
|
262
272
|
const payload = {
|
|
263
273
|
ts: new Date().toISOString(),
|
|
264
274
|
action,
|
|
265
|
-
actorUserId
|
|
266
|
-
actorRole
|
|
275
|
+
actorUserId,
|
|
276
|
+
actorRole,
|
|
267
277
|
workspaceId: requestWorkspaceId(req),
|
|
268
278
|
details
|
|
269
279
|
};
|
|
@@ -140,6 +140,49 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
140
140
|
expect(task?.status).toBe('in-progress');
|
|
141
141
|
expect(defaultWorkspaceTasks.find((item) => item.id === 'task-cloud-1')).toBeUndefined();
|
|
142
142
|
});
|
|
143
|
+
it('lets the cloud task keep a colliding reference number during apply-local and records a diagnostic', async () => {
|
|
144
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-task-ref-collision-');
|
|
145
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
146
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
147
|
+
const localTask = core.createTask({ title: 'Local incumbent' }, { workspaceId });
|
|
148
|
+
expect(localTask.referenceLabel).toBe('T-1');
|
|
149
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
150
|
+
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
151
|
+
expect(match).toBeTruthy();
|
|
152
|
+
const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
|
|
153
|
+
changes: [
|
|
154
|
+
{
|
|
155
|
+
op: 'upsert',
|
|
156
|
+
archived: false,
|
|
157
|
+
task: {
|
|
158
|
+
id: 'task-cloud-ref-1',
|
|
159
|
+
title: 'Cloud winner',
|
|
160
|
+
status: 'task',
|
|
161
|
+
referenceNumber: 1,
|
|
162
|
+
updatedAt: '2026-04-10T16:00:00.000Z'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}, { 'x-taskforce-workspace-id': workspaceId });
|
|
167
|
+
const res = createMockRes();
|
|
168
|
+
await match?.handler(req, res, {});
|
|
169
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
170
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
171
|
+
expect(reloadedCore.getTask('task-cloud-ref-1', workspaceId)?.referenceLabel).toBe('T-1');
|
|
172
|
+
expect(reloadedCore.getTask(localTask.id, workspaceId)?.referenceLabel).toBe('T-2');
|
|
173
|
+
expect(reloadedCore.listSyncEvents(workspaceId, 10)).toEqual(expect.arrayContaining([
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
eventType: 'apply',
|
|
176
|
+
status: 'error',
|
|
177
|
+
errorMessage: 'Detected task reference number mismatch during normal sync.',
|
|
178
|
+
details: expect.objectContaining({
|
|
179
|
+
referenceLabel: 'T-1',
|
|
180
|
+
existingTaskId: localTask.id,
|
|
181
|
+
taskId: 'task-cloud-ref-1'
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
]));
|
|
185
|
+
});
|
|
143
186
|
it('persists synced comments via apply-local route', async () => {
|
|
144
187
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-comments-');
|
|
145
188
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
@@ -1268,6 +1311,166 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1268
1311
|
})
|
|
1269
1312
|
]);
|
|
1270
1313
|
});
|
|
1314
|
+
it('lets the cloud document ref win during normal sync apply and records a diagnostic', async () => {
|
|
1315
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
|
|
1316
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1317
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
1318
|
+
core.createTask({
|
|
1319
|
+
id: 'task-doc-ref-drift-1',
|
|
1320
|
+
title: 'Task with drifting synced markdown asset',
|
|
1321
|
+
status: 'task'
|
|
1322
|
+
}, {
|
|
1323
|
+
workspaceId,
|
|
1324
|
+
allowClientProvidedId: true
|
|
1325
|
+
});
|
|
1326
|
+
const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
1327
|
+
seededStore.upsertAsset({
|
|
1328
|
+
assetId: 'asset-doc-ref-drift-1',
|
|
1329
|
+
workspaceId,
|
|
1330
|
+
kind: 'document',
|
|
1331
|
+
originalFilename: 'phase1-ref-drift.md',
|
|
1332
|
+
mimeType: 'text/markdown',
|
|
1333
|
+
storageProvider: 'local',
|
|
1334
|
+
storageKey: 'docs/phase1-ref-drift.md',
|
|
1335
|
+
contentSha256: 'sha-doc-ref-drift-local',
|
|
1336
|
+
sizeBytes: 10,
|
|
1337
|
+
documentId: 'doc-ref-drift-1',
|
|
1338
|
+
referenceNumber: 12,
|
|
1339
|
+
createdAt: '2026-03-17T11:55:00.000Z',
|
|
1340
|
+
updatedAt: '2026-03-17T11:55:00.000Z'
|
|
1341
|
+
});
|
|
1342
|
+
seededStore.upsertLink({
|
|
1343
|
+
workspaceId,
|
|
1344
|
+
assetId: 'asset-doc-ref-drift-1',
|
|
1345
|
+
taskId: 'task-doc-ref-drift-1',
|
|
1346
|
+
role: 'attachment',
|
|
1347
|
+
updatedAt: '2026-03-17T11:55:00.000Z',
|
|
1348
|
+
deletedAt: null
|
|
1349
|
+
});
|
|
1350
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
1351
|
+
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
1352
|
+
expect(match).toBeTruthy();
|
|
1353
|
+
const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
|
|
1354
|
+
changes: [
|
|
1355
|
+
{
|
|
1356
|
+
op: 'document-upsert',
|
|
1357
|
+
path: 'docs/phase1-ref-drift.md',
|
|
1358
|
+
updatedAt: '2026-03-17T12:00:00.000Z',
|
|
1359
|
+
content: '# cloud canonical markdown\n',
|
|
1360
|
+
assetId: 'asset-doc-ref-drift-1',
|
|
1361
|
+
documentId: 'doc-ref-drift-1',
|
|
1362
|
+
referenceNumber: 18,
|
|
1363
|
+
version: 2,
|
|
1364
|
+
taskId: 'task-doc-ref-drift-1',
|
|
1365
|
+
originalFilename: 'phase1-ref-drift.md',
|
|
1366
|
+
linkRole: 'attachment'
|
|
1367
|
+
}
|
|
1368
|
+
]
|
|
1369
|
+
}, { 'x-taskforce-workspace-id': workspaceId });
|
|
1370
|
+
const res = createMockRes();
|
|
1371
|
+
await match?.handler(req, res, {});
|
|
1372
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1373
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1374
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1375
|
+
const asset = store.get('asset-doc-ref-drift-1', workspaceId);
|
|
1376
|
+
const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
|
|
1377
|
+
expect(asset).toEqual(expect.objectContaining({
|
|
1378
|
+
assetId: 'asset-doc-ref-drift-1',
|
|
1379
|
+
referenceNumber: 18,
|
|
1380
|
+
version: 2
|
|
1381
|
+
}));
|
|
1382
|
+
expect(syncEvents).toEqual(expect.arrayContaining([
|
|
1383
|
+
expect.objectContaining({
|
|
1384
|
+
eventType: 'apply',
|
|
1385
|
+
status: 'error',
|
|
1386
|
+
errorMessage: 'Detected document reference number mismatch during normal sync.',
|
|
1387
|
+
details: expect.objectContaining({
|
|
1388
|
+
assetId: 'asset-doc-ref-drift-1',
|
|
1389
|
+
existingReferenceNumber: 12,
|
|
1390
|
+
incomingReferenceNumber: 18
|
|
1391
|
+
})
|
|
1392
|
+
})
|
|
1393
|
+
]));
|
|
1394
|
+
});
|
|
1395
|
+
it('lets the cloud image ref win during normal sync apply and records a diagnostic', async () => {
|
|
1396
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
|
|
1397
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1398
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
1399
|
+
core.createTask({
|
|
1400
|
+
id: 'task-image-ref-drift-1',
|
|
1401
|
+
title: 'Task with drifting synced image asset',
|
|
1402
|
+
status: 'task'
|
|
1403
|
+
}, {
|
|
1404
|
+
workspaceId,
|
|
1405
|
+
allowClientProvidedId: true
|
|
1406
|
+
});
|
|
1407
|
+
const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
1408
|
+
seededStore.upsertAsset({
|
|
1409
|
+
assetId: 'asset-image-ref-drift-1',
|
|
1410
|
+
workspaceId,
|
|
1411
|
+
kind: 'image',
|
|
1412
|
+
originalFilename: 'hero.png',
|
|
1413
|
+
mimeType: 'image/png',
|
|
1414
|
+
storageProvider: 'local',
|
|
1415
|
+
storageKey: 'workspaces/workspace-local-active/assets/images/hero.png',
|
|
1416
|
+
contentSha256: 'sha-image-ref-drift-local',
|
|
1417
|
+
sizeBytes: 10,
|
|
1418
|
+
referenceNumber: 12,
|
|
1419
|
+
createdAt: '2026-03-17T11:55:00.000Z',
|
|
1420
|
+
updatedAt: '2026-03-17T11:55:00.000Z'
|
|
1421
|
+
});
|
|
1422
|
+
seededStore.upsertLink({
|
|
1423
|
+
workspaceId,
|
|
1424
|
+
assetId: 'asset-image-ref-drift-1',
|
|
1425
|
+
taskId: 'task-image-ref-drift-1',
|
|
1426
|
+
role: 'image',
|
|
1427
|
+
updatedAt: '2026-03-17T11:55:00.000Z',
|
|
1428
|
+
deletedAt: null
|
|
1429
|
+
});
|
|
1430
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
1431
|
+
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
1432
|
+
expect(match).toBeTruthy();
|
|
1433
|
+
const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
|
|
1434
|
+
changes: [
|
|
1435
|
+
{
|
|
1436
|
+
op: 'asset-upsert',
|
|
1437
|
+
path: 'workspaces/workspace-local-active/assets/images/hero.png',
|
|
1438
|
+
updatedAt: '2026-03-17T12:05:00.000Z',
|
|
1439
|
+
contentBase64: Buffer.from('cloud-hero').toString('base64'),
|
|
1440
|
+
assetId: 'asset-image-ref-drift-1',
|
|
1441
|
+
kind: 'image',
|
|
1442
|
+
mimeType: 'image/png',
|
|
1443
|
+
referenceNumber: 18,
|
|
1444
|
+
taskId: 'task-image-ref-drift-1',
|
|
1445
|
+
originalFilename: 'hero.png',
|
|
1446
|
+
linkRole: 'image'
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
}, { 'x-taskforce-workspace-id': workspaceId });
|
|
1450
|
+
const res = createMockRes();
|
|
1451
|
+
await match?.handler(req, res, {});
|
|
1452
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1453
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1454
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1455
|
+
const asset = store.get('asset-image-ref-drift-1', workspaceId);
|
|
1456
|
+
const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
|
|
1457
|
+
expect(asset).toEqual(expect.objectContaining({
|
|
1458
|
+
assetId: 'asset-image-ref-drift-1',
|
|
1459
|
+
referenceNumber: 18
|
|
1460
|
+
}));
|
|
1461
|
+
expect(syncEvents).toEqual(expect.arrayContaining([
|
|
1462
|
+
expect.objectContaining({
|
|
1463
|
+
eventType: 'apply',
|
|
1464
|
+
status: 'error',
|
|
1465
|
+
errorMessage: 'Detected asset reference number mismatch during normal sync.',
|
|
1466
|
+
details: expect.objectContaining({
|
|
1467
|
+
assetId: 'asset-image-ref-drift-1',
|
|
1468
|
+
existingReferenceNumber: 12,
|
|
1469
|
+
incomingReferenceNumber: 18
|
|
1470
|
+
})
|
|
1471
|
+
})
|
|
1472
|
+
]));
|
|
1473
|
+
});
|
|
1271
1474
|
it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
|
|
1272
1475
|
const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
|
|
1273
1476
|
const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-local-');
|
|
@@ -1487,6 +1690,93 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1487
1690
|
})
|
|
1488
1691
|
]);
|
|
1489
1692
|
});
|
|
1693
|
+
it('omits local provisional reference fields from authenticated pull payloads', async () => {
|
|
1694
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-pull-task-ref-payload-');
|
|
1695
|
+
const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1696
|
+
seedAuthenticatedWorkspace(seedCore);
|
|
1697
|
+
const created = seedCore.createTask({
|
|
1698
|
+
title: 'Offline draft',
|
|
1699
|
+
status: 'task'
|
|
1700
|
+
}, {
|
|
1701
|
+
workspaceId,
|
|
1702
|
+
provisionalLocalReference: true
|
|
1703
|
+
});
|
|
1704
|
+
expect(created.localReferenceNumber).toBe(1);
|
|
1705
|
+
expect(created.referenceLabel).toBe('LT-1');
|
|
1706
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1707
|
+
const routes = createRoutes(core, { authConfig });
|
|
1708
|
+
const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
|
|
1709
|
+
expect(pullMatch).toBeTruthy();
|
|
1710
|
+
const cookie = createAuthCookie(workspaceId);
|
|
1711
|
+
const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
|
|
1712
|
+
const pullRes = createMockRes();
|
|
1713
|
+
await pullMatch?.handler(pullReq, pullRes, {});
|
|
1714
|
+
expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1715
|
+
const pullBody = readJsonBody(pullRes);
|
|
1716
|
+
const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
|
|
1717
|
+
.find((change) => change?.op === 'upsert' && change?.task?.id === created.id);
|
|
1718
|
+
expect(upsert?.task).toEqual(expect.objectContaining({
|
|
1719
|
+
id: created.id,
|
|
1720
|
+
title: 'Offline draft',
|
|
1721
|
+
referenceNumber: null
|
|
1722
|
+
}));
|
|
1723
|
+
expect(upsert?.task).not.toHaveProperty('localReferenceNumber');
|
|
1724
|
+
expect(upsert?.task).not.toHaveProperty('referenceLabel');
|
|
1725
|
+
});
|
|
1726
|
+
it('returns a pushed provisional task on pull even when the caller cursor is newer than the stale local watermark', async () => {
|
|
1727
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-provisional-task-');
|
|
1728
|
+
const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1729
|
+
seedAuthenticatedWorkspace(seedCore);
|
|
1730
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1731
|
+
const routes = createRoutes(core, { authConfig });
|
|
1732
|
+
const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
|
|
1733
|
+
const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
|
|
1734
|
+
expect(pushMatch).toBeTruthy();
|
|
1735
|
+
expect(pullMatch).toBeTruthy();
|
|
1736
|
+
const cookie = createAuthCookie(workspaceId);
|
|
1737
|
+
const taskId = 'task-provisional-roundtrip-1';
|
|
1738
|
+
const staleUpdatedAt = '2026-04-11T21:29:04.121Z';
|
|
1739
|
+
const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
|
|
1740
|
+
workspaceId,
|
|
1741
|
+
idempotencyKey: 'task-provisional-roundtrip-1',
|
|
1742
|
+
changes: [
|
|
1743
|
+
{
|
|
1744
|
+
op: 'upsert',
|
|
1745
|
+
archived: false,
|
|
1746
|
+
task: {
|
|
1747
|
+
id: taskId,
|
|
1748
|
+
title: 'Offline provisional task',
|
|
1749
|
+
status: 'task',
|
|
1750
|
+
createdAt: '2026-04-11T21:28:52.410Z',
|
|
1751
|
+
updatedAt: staleUpdatedAt,
|
|
1752
|
+
referenceNumber: null
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
]
|
|
1756
|
+
}, { cookie });
|
|
1757
|
+
const pushRes = createMockRes();
|
|
1758
|
+
await pushMatch?.handler(pushReq, pushRes, {});
|
|
1759
|
+
expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1760
|
+
const persisted = core.getTask(taskId, workspaceId);
|
|
1761
|
+
expect(persisted?.referenceNumber).toBe(1);
|
|
1762
|
+
expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse(staleUpdatedAt));
|
|
1763
|
+
const cursor = Buffer.from(JSON.stringify({
|
|
1764
|
+
u: '2026-04-11T21:29:05.000Z',
|
|
1765
|
+
id: 'seed'
|
|
1766
|
+
})).toString('base64url');
|
|
1767
|
+
const pullReq = createMockReq('GET', `/api/taskforce/sync/workspace/pull?workspaceId=${workspaceId}&limit=200&cursor=${cursor}`, undefined, { cookie });
|
|
1768
|
+
const pullRes = createMockRes();
|
|
1769
|
+
await pullMatch?.handler(pullReq, pullRes, {});
|
|
1770
|
+
expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1771
|
+
const pullBody = readJsonBody(pullRes);
|
|
1772
|
+
const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
|
|
1773
|
+
.find((change) => change?.op === 'upsert' && change?.task?.id === taskId);
|
|
1774
|
+
expect(upsert?.task).toEqual(expect.objectContaining({
|
|
1775
|
+
id: taskId,
|
|
1776
|
+
title: 'Offline provisional task',
|
|
1777
|
+
referenceNumber: 1
|
|
1778
|
+
}));
|
|
1779
|
+
});
|
|
1490
1780
|
it('keeps document deleted when push batch includes upsert and delete for same path', async () => {
|
|
1491
1781
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-doc-delete-wins-');
|
|
1492
1782
|
const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|