@taskforcehq/taskforce 0.3.305 → 0.3.306
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 +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -3,6 +3,7 @@ import { Readable } from 'stream';
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
6
7
|
import { TaskforceCore } from '../../core/Taskforce';
|
|
7
8
|
import { createRoutes, matchRoute } from '../routes';
|
|
8
9
|
import { createSessionTokenCookie } from '../auth';
|
|
@@ -110,6 +111,7 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
110
111
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-');
|
|
111
112
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
112
113
|
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
114
|
+
const basePath = core.getPaths().basePath;
|
|
113
115
|
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
114
116
|
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
115
117
|
expect(match).toBeTruthy();
|
|
@@ -344,6 +346,8 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
344
346
|
username: 'codex',
|
|
345
347
|
icon: 'Bot',
|
|
346
348
|
color: '#0ea5e9',
|
|
349
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
|
|
350
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp',
|
|
347
351
|
description: 'Terminal-first coding agent',
|
|
348
352
|
role: 'Implementer',
|
|
349
353
|
provider: 'OpenAI',
|
|
@@ -369,7 +373,9 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
369
373
|
role: 'Implementer',
|
|
370
374
|
provider: 'OpenAI',
|
|
371
375
|
model: 'gpt-5.4',
|
|
372
|
-
surfaceType: 'coding_tool'
|
|
376
|
+
surfaceType: 'coding_tool',
|
|
377
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
|
|
378
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp'
|
|
373
379
|
})
|
|
374
380
|
]);
|
|
375
381
|
});
|
|
@@ -1319,7 +1325,7 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1319
1325
|
})
|
|
1320
1326
|
]);
|
|
1321
1327
|
});
|
|
1322
|
-
it('lets the cloud document ref win during normal sync apply
|
|
1328
|
+
it('lets the cloud document ref win during normal sync apply without recording a sync error', async () => {
|
|
1323
1329
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
|
|
1324
1330
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1325
1331
|
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
@@ -1381,26 +1387,17 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1381
1387
|
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1382
1388
|
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1383
1389
|
const asset = store.get('asset-doc-ref-drift-1', workspaceId);
|
|
1384
|
-
const
|
|
1390
|
+
const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
|
|
1391
|
+
&& event.status === 'error'
|
|
1392
|
+
&& event.errorMessage === 'Detected document reference number mismatch during normal sync.');
|
|
1385
1393
|
expect(asset).toEqual(expect.objectContaining({
|
|
1386
1394
|
assetId: 'asset-doc-ref-drift-1',
|
|
1387
1395
|
referenceNumber: 18,
|
|
1388
1396
|
version: 2
|
|
1389
1397
|
}));
|
|
1390
|
-
expect(
|
|
1391
|
-
expect.objectContaining({
|
|
1392
|
-
eventType: 'apply',
|
|
1393
|
-
status: 'error',
|
|
1394
|
-
errorMessage: 'Detected document reference number mismatch during normal sync.',
|
|
1395
|
-
details: expect.objectContaining({
|
|
1396
|
-
assetId: 'asset-doc-ref-drift-1',
|
|
1397
|
-
existingReferenceNumber: 12,
|
|
1398
|
-
incomingReferenceNumber: 18
|
|
1399
|
-
})
|
|
1400
|
-
})
|
|
1401
|
-
]));
|
|
1398
|
+
expect(mismatchEvents).toHaveLength(0);
|
|
1402
1399
|
});
|
|
1403
|
-
it('lets the cloud image ref win during normal sync apply
|
|
1400
|
+
it('lets the cloud image ref win during normal sync apply without recording a sync error', async () => {
|
|
1404
1401
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
|
|
1405
1402
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1406
1403
|
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
@@ -1461,23 +1458,94 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1461
1458
|
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1462
1459
|
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1463
1460
|
const asset = store.get('asset-image-ref-drift-1', workspaceId);
|
|
1464
|
-
const
|
|
1461
|
+
const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
|
|
1462
|
+
&& event.status === 'error'
|
|
1463
|
+
&& event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
|
|
1465
1464
|
expect(asset).toEqual(expect.objectContaining({
|
|
1466
1465
|
assetId: 'asset-image-ref-drift-1',
|
|
1467
1466
|
referenceNumber: 18
|
|
1468
1467
|
}));
|
|
1469
|
-
expect(
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1468
|
+
expect(mismatchEvents).toHaveLength(0);
|
|
1469
|
+
});
|
|
1470
|
+
it('converges unchanged image reference drift during normal sync so later pulls stop re-logging it', async () => {
|
|
1471
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-converge-');
|
|
1472
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1473
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
1474
|
+
core.createTask({
|
|
1475
|
+
id: 'task-image-ref-converge-1',
|
|
1476
|
+
title: 'Task with converging synced image asset',
|
|
1477
|
+
status: 'task'
|
|
1478
|
+
}, {
|
|
1479
|
+
workspaceId,
|
|
1480
|
+
allowClientProvidedId: true
|
|
1481
|
+
});
|
|
1482
|
+
const basePath = core.getPaths().basePath;
|
|
1483
|
+
const relativeAssetPath = 'workspaces/workspace-local-active/assets/images/hero-converge.png';
|
|
1484
|
+
const absoluteAssetPath = path.join(basePath, relativeAssetPath);
|
|
1485
|
+
fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
|
|
1486
|
+
fs.writeFileSync(absoluteAssetPath, 'cloud-hero');
|
|
1487
|
+
const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
1488
|
+
seededStore.upsertAsset({
|
|
1489
|
+
assetId: 'asset-image-ref-converge-1',
|
|
1490
|
+
workspaceId,
|
|
1491
|
+
kind: 'image',
|
|
1492
|
+
originalFilename: 'hero-converge.png',
|
|
1493
|
+
mimeType: 'image/png',
|
|
1494
|
+
storageProvider: 'local',
|
|
1495
|
+
storageKey: relativeAssetPath,
|
|
1496
|
+
contentSha256: createHash('sha256').update('cloud-hero').digest('hex'),
|
|
1497
|
+
sizeBytes: Buffer.byteLength('cloud-hero'),
|
|
1498
|
+
referenceNumber: 12,
|
|
1499
|
+
createdAt: '2026-03-17T11:55:00.000Z',
|
|
1500
|
+
updatedAt: '2026-03-17T12:05:00.000Z'
|
|
1501
|
+
});
|
|
1502
|
+
seededStore.upsertLink({
|
|
1503
|
+
workspaceId,
|
|
1504
|
+
assetId: 'asset-image-ref-converge-1',
|
|
1505
|
+
taskId: 'task-image-ref-converge-1',
|
|
1506
|
+
role: 'image',
|
|
1507
|
+
updatedAt: '2026-03-17T11:55:00.000Z',
|
|
1508
|
+
deletedAt: null
|
|
1509
|
+
});
|
|
1510
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
1511
|
+
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
1512
|
+
expect(match).toBeTruthy();
|
|
1513
|
+
const body = {
|
|
1514
|
+
changes: [
|
|
1515
|
+
{
|
|
1516
|
+
op: 'asset-upsert',
|
|
1517
|
+
path: relativeAssetPath,
|
|
1518
|
+
updatedAt: '2026-03-17T12:05:00.000Z',
|
|
1519
|
+
contentBase64: Buffer.from('cloud-hero').toString('base64'),
|
|
1520
|
+
assetId: 'asset-image-ref-converge-1',
|
|
1521
|
+
kind: 'image',
|
|
1522
|
+
mimeType: 'image/png',
|
|
1523
|
+
referenceNumber: 18,
|
|
1524
|
+
taskId: 'task-image-ref-converge-1',
|
|
1525
|
+
originalFilename: 'hero-converge.png',
|
|
1526
|
+
linkRole: 'image'
|
|
1527
|
+
}
|
|
1528
|
+
]
|
|
1529
|
+
};
|
|
1530
|
+
const firstReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
|
|
1531
|
+
const firstRes = createMockRes();
|
|
1532
|
+
await match?.handler(firstReq, firstRes, {});
|
|
1533
|
+
expect(firstRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1534
|
+
const secondReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
|
|
1535
|
+
const secondRes = createMockRes();
|
|
1536
|
+
await match?.handler(secondReq, secondRes, {});
|
|
1537
|
+
expect(secondRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1538
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1539
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
1540
|
+
const asset = store.get('asset-image-ref-converge-1', workspaceId);
|
|
1541
|
+
const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
|
|
1542
|
+
&& event.status === 'error'
|
|
1543
|
+
&& event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
|
|
1544
|
+
expect(asset).toEqual(expect.objectContaining({
|
|
1545
|
+
assetId: 'asset-image-ref-converge-1',
|
|
1546
|
+
referenceNumber: 18
|
|
1547
|
+
}));
|
|
1548
|
+
expect(mismatchEvents).toHaveLength(0);
|
|
1481
1549
|
});
|
|
1482
1550
|
it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
|
|
1483
1551
|
const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
|
|
@@ -1992,9 +2060,9 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
1992
2060
|
workspaceId,
|
|
1993
2061
|
eventType: 'apply',
|
|
1994
2062
|
status: 'error',
|
|
1995
|
-
errorMessage: 'Detected
|
|
2063
|
+
errorMessage: 'Detected task reference number mismatch during normal sync.',
|
|
1996
2064
|
details: {
|
|
1997
|
-
|
|
2065
|
+
taskId: 'task-1',
|
|
1998
2066
|
existingReferenceNumber: 12,
|
|
1999
2067
|
incomingReferenceNumber: 18
|
|
2000
2068
|
}
|
|
@@ -2013,15 +2081,319 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
2013
2081
|
expect.objectContaining({
|
|
2014
2082
|
eventType: 'apply',
|
|
2015
2083
|
status: 'error',
|
|
2016
|
-
errorMessage: 'Detected
|
|
2084
|
+
errorMessage: 'Detected task reference number mismatch during normal sync.',
|
|
2017
2085
|
details: expect.objectContaining({
|
|
2018
|
-
|
|
2086
|
+
taskId: 'task-1',
|
|
2019
2087
|
existingReferenceNumber: 12,
|
|
2020
2088
|
incomingReferenceNumber: 18
|
|
2021
2089
|
})
|
|
2022
2090
|
})
|
|
2023
2091
|
]));
|
|
2024
2092
|
});
|
|
2093
|
+
it('repairs orphaned startup document and asset metadata and exposes the recovered metadata through snapshots', async () => {
|
|
2094
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-recover-');
|
|
2095
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2096
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
2097
|
+
const basePath = core.getPaths().basePath;
|
|
2098
|
+
const relativeDocPath = 'docs/recovered-startup-note.md';
|
|
2099
|
+
const relativeAssetPath = `workspaces/${workspaceId}/assets/images/recovered-startup-image.png`;
|
|
2100
|
+
const absoluteDocPath = path.join(basePath, relativeDocPath);
|
|
2101
|
+
const absoluteAssetPath = path.join(basePath, relativeAssetPath);
|
|
2102
|
+
const assetBuffer = Buffer.from('recovered-image-binary');
|
|
2103
|
+
fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
|
|
2104
|
+
fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
|
|
2105
|
+
fs.writeFileSync(absoluteDocPath, '# repaired startup note\n', 'utf8');
|
|
2106
|
+
fs.writeFileSync(absoluteAssetPath, assetBuffer);
|
|
2107
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
2108
|
+
const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
|
|
2109
|
+
const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
|
|
2110
|
+
const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
|
|
2111
|
+
expect(documentsMatch).toBeTruthy();
|
|
2112
|
+
expect(assetsMatch).toBeTruthy();
|
|
2113
|
+
expect(repairMatch).toBeTruthy();
|
|
2114
|
+
const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2115
|
+
const beforeDocumentsRes = createMockRes();
|
|
2116
|
+
await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
|
|
2117
|
+
expect(beforeDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2118
|
+
const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
|
|
2119
|
+
const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
|
|
2120
|
+
.find((entry) => entry.path === relativeDocPath);
|
|
2121
|
+
expect(beforeDoc).toBeUndefined();
|
|
2122
|
+
const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2123
|
+
const beforeAssetsRes = createMockRes();
|
|
2124
|
+
await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
|
|
2125
|
+
expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2126
|
+
const beforeAssetsBody = readJsonBody(beforeAssetsRes);
|
|
2127
|
+
expect(beforeAssetsBody.assets).toEqual([]);
|
|
2128
|
+
const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
|
|
2129
|
+
const repairRes = createMockRes();
|
|
2130
|
+
await repairMatch?.handler(repairReq, repairRes, {});
|
|
2131
|
+
expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2132
|
+
expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
|
|
2133
|
+
success: true,
|
|
2134
|
+
applied: 2,
|
|
2135
|
+
skipped: 0,
|
|
2136
|
+
failed: 0
|
|
2137
|
+
}));
|
|
2138
|
+
const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2139
|
+
const afterDocumentsRes = createMockRes();
|
|
2140
|
+
await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
|
|
2141
|
+
expect(afterDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2142
|
+
const afterDocumentsBody = readJsonBody(afterDocumentsRes);
|
|
2143
|
+
const repairedDoc = (Array.isArray(afterDocumentsBody?.documents) ? afterDocumentsBody.documents : [])
|
|
2144
|
+
.find((entry) => entry.path === relativeDocPath);
|
|
2145
|
+
expect(repairedDoc).toEqual(expect.objectContaining({
|
|
2146
|
+
path: relativeDocPath,
|
|
2147
|
+
content: '# repaired startup note\n',
|
|
2148
|
+
assetId: expect.stringMatching(/^asset-/),
|
|
2149
|
+
documentId: expect.stringMatching(/^doc-/),
|
|
2150
|
+
originalFilename: 'recovered-startup-note.md'
|
|
2151
|
+
}));
|
|
2152
|
+
const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2153
|
+
const afterAssetsRes = createMockRes();
|
|
2154
|
+
await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
|
|
2155
|
+
expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2156
|
+
const afterAssetsBody = readJsonBody(afterAssetsRes);
|
|
2157
|
+
const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
|
|
2158
|
+
.find((entry) => entry.path === relativeAssetPath);
|
|
2159
|
+
expect(repairedAsset).toEqual(expect.objectContaining({
|
|
2160
|
+
path: relativeAssetPath,
|
|
2161
|
+
assetId: expect.stringMatching(/^asset-/),
|
|
2162
|
+
kind: 'image',
|
|
2163
|
+
mimeType: 'image/png',
|
|
2164
|
+
originalFilename: 'recovered-startup-image.png',
|
|
2165
|
+
linkRole: 'image',
|
|
2166
|
+
contentBase64: assetBuffer.toString('base64')
|
|
2167
|
+
}));
|
|
2168
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2169
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
2170
|
+
expect(store.getByStorageKey(relativeDocPath, workspaceId)).toEqual(expect.objectContaining({
|
|
2171
|
+
storageKey: relativeDocPath,
|
|
2172
|
+
kind: 'document',
|
|
2173
|
+
mimeType: 'text/markdown',
|
|
2174
|
+
originalFilename: 'recovered-startup-note.md',
|
|
2175
|
+
documentId: expect.stringMatching(/^doc-/)
|
|
2176
|
+
}));
|
|
2177
|
+
expect(store.getByStorageKey(relativeAssetPath, workspaceId)).toEqual(expect.objectContaining({
|
|
2178
|
+
storageKey: relativeAssetPath,
|
|
2179
|
+
kind: 'image',
|
|
2180
|
+
mimeType: 'image/png',
|
|
2181
|
+
originalFilename: 'recovered-startup-image.png'
|
|
2182
|
+
}));
|
|
2183
|
+
});
|
|
2184
|
+
it('revives stale startup tombstones with the original metadata identifiers when local files are newer', async () => {
|
|
2185
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-revive-');
|
|
2186
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2187
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
2188
|
+
const basePath = core.getPaths().basePath;
|
|
2189
|
+
const relativeDocPath = 'docs/revived-startup-note.md';
|
|
2190
|
+
const relativeAssetPath = `workspaces/${workspaceId}/assets/images/revived-startup-image.png`;
|
|
2191
|
+
const absoluteDocPath = path.join(basePath, relativeDocPath);
|
|
2192
|
+
const absoluteAssetPath = path.join(basePath, relativeAssetPath);
|
|
2193
|
+
const revivedAt = new Date('2026-03-17T12:00:00.000Z');
|
|
2194
|
+
const deletedAt = '2026-03-10T08:00:00.000Z';
|
|
2195
|
+
const assetBuffer = Buffer.from('revived-image-binary');
|
|
2196
|
+
fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
|
|
2197
|
+
fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
|
|
2198
|
+
fs.writeFileSync(absoluteDocPath, '# revived startup note\n', 'utf8');
|
|
2199
|
+
fs.writeFileSync(absoluteAssetPath, assetBuffer);
|
|
2200
|
+
fs.utimesSync(absoluteDocPath, revivedAt, revivedAt);
|
|
2201
|
+
fs.utimesSync(absoluteAssetPath, revivedAt, revivedAt);
|
|
2202
|
+
const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
2203
|
+
seededStore.upsertAsset({
|
|
2204
|
+
assetId: 'asset-revived-doc-1',
|
|
2205
|
+
workspaceId,
|
|
2206
|
+
kind: 'document',
|
|
2207
|
+
originalFilename: 'revived-startup-note.md',
|
|
2208
|
+
mimeType: 'text/markdown',
|
|
2209
|
+
storageProvider: 'local',
|
|
2210
|
+
storageKey: relativeDocPath,
|
|
2211
|
+
contentSha256: 'deleted-doc-hash',
|
|
2212
|
+
sizeBytes: 1,
|
|
2213
|
+
documentId: 'doc-revived-startup-1',
|
|
2214
|
+
referenceNumber: 33,
|
|
2215
|
+
version: 4,
|
|
2216
|
+
createdAt: '2026-03-01T08:00:00.000Z',
|
|
2217
|
+
updatedAt: deletedAt,
|
|
2218
|
+
deletedAt,
|
|
2219
|
+
purgeAfter: '2026-06-01T08:00:00.000Z'
|
|
2220
|
+
});
|
|
2221
|
+
seededStore.upsertAsset({
|
|
2222
|
+
assetId: 'asset-revived-image-1',
|
|
2223
|
+
workspaceId,
|
|
2224
|
+
kind: 'image',
|
|
2225
|
+
originalFilename: 'revived-startup-image.png',
|
|
2226
|
+
mimeType: 'image/png',
|
|
2227
|
+
storageProvider: 'local',
|
|
2228
|
+
storageKey: relativeAssetPath,
|
|
2229
|
+
contentSha256: 'deleted-image-hash',
|
|
2230
|
+
sizeBytes: 1,
|
|
2231
|
+
referenceNumber: 21,
|
|
2232
|
+
createdAt: '2026-03-01T08:00:00.000Z',
|
|
2233
|
+
updatedAt: deletedAt,
|
|
2234
|
+
deletedAt,
|
|
2235
|
+
purgeAfter: '2026-06-01T08:00:00.000Z'
|
|
2236
|
+
});
|
|
2237
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
2238
|
+
const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
|
|
2239
|
+
const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
|
|
2240
|
+
const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
|
|
2241
|
+
expect(documentsMatch).toBeTruthy();
|
|
2242
|
+
expect(assetsMatch).toBeTruthy();
|
|
2243
|
+
expect(repairMatch).toBeTruthy();
|
|
2244
|
+
const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2245
|
+
const beforeDocumentsRes = createMockRes();
|
|
2246
|
+
await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
|
|
2247
|
+
const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
|
|
2248
|
+
const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
|
|
2249
|
+
.find((entry) => entry.path === relativeDocPath);
|
|
2250
|
+
expect(beforeDoc).toBeUndefined();
|
|
2251
|
+
const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2252
|
+
const beforeAssetsRes = createMockRes();
|
|
2253
|
+
await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
|
|
2254
|
+
expect(readJsonBody(beforeAssetsRes).assets).toEqual([]);
|
|
2255
|
+
const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
|
|
2256
|
+
const repairRes = createMockRes();
|
|
2257
|
+
await repairMatch?.handler(repairReq, repairRes, {});
|
|
2258
|
+
expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
|
|
2259
|
+
success: true,
|
|
2260
|
+
applied: 2,
|
|
2261
|
+
skipped: 0,
|
|
2262
|
+
failed: 0
|
|
2263
|
+
}));
|
|
2264
|
+
const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2265
|
+
const afterDocumentsRes = createMockRes();
|
|
2266
|
+
await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
|
|
2267
|
+
const repairedDoc = (Array.isArray(readJsonBody(afterDocumentsRes)?.documents) ? readJsonBody(afterDocumentsRes).documents : [])
|
|
2268
|
+
.find((entry) => entry.path === relativeDocPath);
|
|
2269
|
+
expect(repairedDoc).toEqual(expect.objectContaining({
|
|
2270
|
+
path: relativeDocPath,
|
|
2271
|
+
assetId: 'asset-revived-doc-1',
|
|
2272
|
+
documentId: 'doc-revived-startup-1',
|
|
2273
|
+
referenceNumber: 33,
|
|
2274
|
+
version: 5,
|
|
2275
|
+
originalFilename: 'revived-startup-note.md'
|
|
2276
|
+
}));
|
|
2277
|
+
const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2278
|
+
const afterAssetsRes = createMockRes();
|
|
2279
|
+
await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
|
|
2280
|
+
const repairedAsset = (Array.isArray(readJsonBody(afterAssetsRes)?.assets) ? readJsonBody(afterAssetsRes).assets : [])
|
|
2281
|
+
.find((entry) => entry.path === relativeAssetPath);
|
|
2282
|
+
expect(repairedAsset).toEqual(expect.objectContaining({
|
|
2283
|
+
path: relativeAssetPath,
|
|
2284
|
+
assetId: 'asset-revived-image-1',
|
|
2285
|
+
referenceNumber: 21,
|
|
2286
|
+
kind: 'image',
|
|
2287
|
+
mimeType: 'image/png',
|
|
2288
|
+
originalFilename: 'revived-startup-image.png',
|
|
2289
|
+
contentBase64: assetBuffer.toString('base64')
|
|
2290
|
+
}));
|
|
2291
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2292
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
2293
|
+
expect(store.get('asset-revived-doc-1', workspaceId)).toEqual(expect.objectContaining({
|
|
2294
|
+
assetId: 'asset-revived-doc-1',
|
|
2295
|
+
documentId: 'doc-revived-startup-1',
|
|
2296
|
+
referenceNumber: 33,
|
|
2297
|
+
version: 5,
|
|
2298
|
+
deletedAt: null
|
|
2299
|
+
}));
|
|
2300
|
+
expect(store.get('asset-revived-image-1', workspaceId)).toEqual(expect.objectContaining({
|
|
2301
|
+
assetId: 'asset-revived-image-1',
|
|
2302
|
+
referenceNumber: 21,
|
|
2303
|
+
deletedAt: null
|
|
2304
|
+
}));
|
|
2305
|
+
});
|
|
2306
|
+
it('removes stale startup asset links to missing tasks while preserving the asset snapshot', async () => {
|
|
2307
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-stale-link-');
|
|
2308
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2309
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
2310
|
+
const basePath = core.getPaths().basePath;
|
|
2311
|
+
const relativeAssetPath = `workspaces/${workspaceId}/assets/files/stale-link-asset.txt`;
|
|
2312
|
+
const absoluteAssetPath = path.join(basePath, relativeAssetPath);
|
|
2313
|
+
const assetBuffer = Buffer.from('stale-link-asset');
|
|
2314
|
+
fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
|
|
2315
|
+
fs.writeFileSync(absoluteAssetPath, assetBuffer);
|
|
2316
|
+
const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
2317
|
+
seededStore.upsertAsset({
|
|
2318
|
+
assetId: 'asset-stale-link-1',
|
|
2319
|
+
workspaceId,
|
|
2320
|
+
kind: 'file',
|
|
2321
|
+
originalFilename: 'stale-link-asset.txt',
|
|
2322
|
+
mimeType: 'text/plain',
|
|
2323
|
+
storageProvider: 'local',
|
|
2324
|
+
storageKey: relativeAssetPath,
|
|
2325
|
+
contentSha256: createHash('sha256').update(assetBuffer).digest('hex'),
|
|
2326
|
+
sizeBytes: assetBuffer.length,
|
|
2327
|
+
createdAt: '2026-03-17T11:00:00.000Z',
|
|
2328
|
+
updatedAt: '2026-03-17T11:00:00.000Z'
|
|
2329
|
+
});
|
|
2330
|
+
seededStore.upsertLink({
|
|
2331
|
+
workspaceId,
|
|
2332
|
+
assetId: 'asset-stale-link-1',
|
|
2333
|
+
taskId: 'task-missing-link-1',
|
|
2334
|
+
role: 'attachment',
|
|
2335
|
+
updatedAt: '2026-03-17T11:00:00.000Z'
|
|
2336
|
+
});
|
|
2337
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
2338
|
+
const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
|
|
2339
|
+
const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
|
|
2340
|
+
expect(assetsMatch).toBeTruthy();
|
|
2341
|
+
expect(repairMatch).toBeTruthy();
|
|
2342
|
+
const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2343
|
+
const beforeAssetsRes = createMockRes();
|
|
2344
|
+
await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
|
|
2345
|
+
expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2346
|
+
const beforeAssetsBody = readJsonBody(beforeAssetsRes);
|
|
2347
|
+
const beforeAsset = (Array.isArray(beforeAssetsBody?.assets) ? beforeAssetsBody.assets : [])
|
|
2348
|
+
.find((entry) => entry.path === relativeAssetPath);
|
|
2349
|
+
expect(beforeAsset).toEqual(expect.objectContaining({
|
|
2350
|
+
path: relativeAssetPath,
|
|
2351
|
+
assetId: 'asset-stale-link-1',
|
|
2352
|
+
taskId: 'task-missing-link-1',
|
|
2353
|
+
linkRole: 'attachment',
|
|
2354
|
+
contentBase64: assetBuffer.toString('base64')
|
|
2355
|
+
}));
|
|
2356
|
+
const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
|
|
2357
|
+
const repairRes = createMockRes();
|
|
2358
|
+
await repairMatch?.handler(repairReq, repairRes, {});
|
|
2359
|
+
expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2360
|
+
expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
|
|
2361
|
+
success: true,
|
|
2362
|
+
applied: 1,
|
|
2363
|
+
skipped: 0,
|
|
2364
|
+
failed: 0
|
|
2365
|
+
}));
|
|
2366
|
+
const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2367
|
+
const afterAssetsRes = createMockRes();
|
|
2368
|
+
await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
|
|
2369
|
+
expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2370
|
+
const afterAssetsBody = readJsonBody(afterAssetsRes);
|
|
2371
|
+
const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
|
|
2372
|
+
.find((entry) => entry.path === relativeAssetPath);
|
|
2373
|
+
expect(repairedAsset).toEqual(expect.objectContaining({
|
|
2374
|
+
path: relativeAssetPath,
|
|
2375
|
+
assetId: 'asset-stale-link-1',
|
|
2376
|
+
taskId: null,
|
|
2377
|
+
linkRole: 'attachment',
|
|
2378
|
+
contentBase64: assetBuffer.toString('base64')
|
|
2379
|
+
}));
|
|
2380
|
+
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2381
|
+
const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
|
|
2382
|
+
expect(store.get('asset-stale-link-1', workspaceId)).toEqual(expect.objectContaining({
|
|
2383
|
+
assetId: 'asset-stale-link-1',
|
|
2384
|
+
storageKey: relativeAssetPath,
|
|
2385
|
+
deletedAt: null
|
|
2386
|
+
}));
|
|
2387
|
+
expect(store.listLinksForAsset('asset-stale-link-1', workspaceId)).toEqual([]);
|
|
2388
|
+
expect(store.listLinksForAsset('asset-stale-link-1', workspaceId, { includeDeleted: true })).toEqual([
|
|
2389
|
+
expect.objectContaining({
|
|
2390
|
+
assetId: 'asset-stale-link-1',
|
|
2391
|
+
taskId: 'task-missing-link-1',
|
|
2392
|
+
role: 'attachment',
|
|
2393
|
+
deletedAt: expect.any(String)
|
|
2394
|
+
})
|
|
2395
|
+
]);
|
|
2396
|
+
});
|
|
2025
2397
|
it('returns 400 on apply-local when workspaceId is null/empty/default', async () => {
|
|
2026
2398
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-wsmissing-');
|
|
2027
2399
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
@@ -2038,16 +2410,24 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
2038
2410
|
it('keeps repeated document scans correct and invalidates cache when file content changes', async () => {
|
|
2039
2411
|
const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-cache-');
|
|
2040
2412
|
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2413
|
+
const basePath = core.getPaths().basePath;
|
|
2041
2414
|
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
2042
|
-
const syncRoot = core.getPaths().basePath;
|
|
2043
|
-
const docsDir = path.join(syncRoot, 'docs');
|
|
2044
|
-
fs.mkdirSync(docsDir, { recursive: true });
|
|
2045
|
-
const relativeDocPath = 'docs/cache-target.md';
|
|
2046
|
-
const absoluteDocPath = path.join(syncRoot, relativeDocPath);
|
|
2047
|
-
fs.writeFileSync(absoluteDocPath, '# cache test\n', 'utf8');
|
|
2048
2415
|
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
2416
|
+
const uploadMatch = matchRoute('POST', '/api/taskforce/context-upload', routes);
|
|
2049
2417
|
const match = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
|
|
2418
|
+
expect(uploadMatch).toBeTruthy();
|
|
2050
2419
|
expect(match).toBeTruthy();
|
|
2420
|
+
const uploadReq = createMockReq('POST', '/api/taskforce/context-upload', {
|
|
2421
|
+
file: `data:text/markdown;base64,${Buffer.from('# cache test\n', 'utf8').toString('base64')}`,
|
|
2422
|
+
originalName: 'cache-target.md',
|
|
2423
|
+
workspaceId
|
|
2424
|
+
}, { 'x-taskforce-workspace-id': workspaceId });
|
|
2425
|
+
const uploadRes = createMockRes();
|
|
2426
|
+
await uploadMatch?.handler(uploadReq, uploadRes, {});
|
|
2427
|
+
expect(uploadRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
|
|
2428
|
+
const uploadBody = JSON.parse(String(uploadRes.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
2429
|
+
const relativeDocPath = String(uploadBody?.fsPath || '');
|
|
2430
|
+
const absoluteDocPath = path.join(basePath, relativeDocPath);
|
|
2051
2431
|
const req1 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2052
2432
|
const res1 = createMockRes();
|
|
2053
2433
|
await match?.handler(req1, res1, {});
|
|
@@ -2102,6 +2482,24 @@ describe('Server Routes Workspace Sync Integration', () => {
|
|
|
2102
2482
|
expect(doc).toBeTruthy();
|
|
2103
2483
|
expect(doc.content).toBe('# local attachment\n');
|
|
2104
2484
|
});
|
|
2485
|
+
it('does not sync raw docs directory markdown files because shared document sync must be canonical and workspace-scoped', async () => {
|
|
2486
|
+
const projectRoot = createIsolatedProjectRoot('taskforce-sync-exclude-raw-docs-');
|
|
2487
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
2488
|
+
core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
|
|
2489
|
+
const rawDocsDir = path.join(core.getPaths().basePath, 'docs');
|
|
2490
|
+
fs.mkdirSync(rawDocsDir, { recursive: true });
|
|
2491
|
+
fs.writeFileSync(path.join(rawDocsDir, 'legacy-shared.md'), '# legacy shared doc\n', 'utf8');
|
|
2492
|
+
const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
2493
|
+
const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
|
|
2494
|
+
expect(documentsMatch).toBeTruthy();
|
|
2495
|
+
const req = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
|
|
2496
|
+
const res = createMockRes();
|
|
2497
|
+
await documentsMatch?.handler(req, res, {});
|
|
2498
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
2499
|
+
const body = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
2500
|
+
const docs = Array.isArray(body?.documents) ? body.documents : [];
|
|
2501
|
+
expect(docs.some((entry) => entry.path === 'docs/legacy-shared.md')).toBe(false);
|
|
2502
|
+
});
|
|
2105
2503
|
it('converges task conflicts deterministically for equal timestamps regardless of apply order', async () => {
|
|
2106
2504
|
const applySequence = async (prefix, titlesInOrder) => {
|
|
2107
2505
|
const projectRoot = createIsolatedProjectRoot(prefix);
|