@taskforcehq/taskforce 0.3.305 → 0.3.307
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/README.md +0 -2
- package/dist/Taskforce.module.css +603 -107
- package/dist/TaskforceCore.js +216 -43
- package/dist/TaskforceCore.test.js +611 -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/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +561 -0
- package/dist/components/features/AgentsModule.d.ts +14 -1
- package/dist/components/features/AgentsModule.js +366 -18
- package/dist/components/features/AgentsModule.test.js +507 -8
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +7 -1
- package/dist/components/features/DocumentViewer.js +62 -4
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +51 -1
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +24 -29
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +61 -116
- 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 -21
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +78 -14
- package/dist/components/views/PlanComparisonPage.test.js +257 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +529 -592
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
- package/dist/components/views/panels/PlanningDrawer.js +270 -55
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +529 -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/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +46 -2
- package/dist/core/AiProfileService.js +1073 -82
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +1249 -44
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +110 -23
- 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 +127 -6
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- 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 +418 -4
- 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/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +415 -40
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +1543 -266
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/Taskforce.mcpAuth.test.js +109 -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 +75 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- 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 +13 -4
- package/dist/hooks/sync/orchestratorShared.js +129 -48
- 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 +179 -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 +16 -3
- package/dist/hooks/sync/transfers.js +210 -66
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +928 -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/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +409 -183
- 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 +145 -52
- package/dist/hooks/useTaskforce.js +592 -253
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +13 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
- 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/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +116 -10
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.js +1099 -185
- package/dist/mcp/runtime.test.js +1491 -140
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +192 -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/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
- 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 +124 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +111 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +459 -28
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.js +1170 -71
- package/dist/server/routes/authSupport.d.ts +5 -1
- package/dist/server/routes/authSupport.js +13 -2
- package/dist/server/routes/billing.js +372 -35
- package/dist/server/routes/billing.test.js +595 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +182 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -14
- package/dist/server/routes/sync.integration.test.js +658 -39
- package/dist/server/routes/sync.js +74 -608
- 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 +182 -0
- package/dist/server/routes/tasks.js +55 -10
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +147 -11
- package/dist/server/routes.test.js +2704 -183
- 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/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- 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/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- 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 +19 -11
- package/dist/sync/collaborationSyncPayload.test.js +14 -1
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +9 -9
- package/dist/sync/contentSyncState.js +248 -107
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +11 -4
- package/dist/sync/syncApplyHandlers.js +115 -42
- package/dist/sync/syncApplyHandlers.test.js +251 -4
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +46 -0
- package/dist/sync/syncResourceAdapters.js +79 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +65 -1
- package/dist/sync/syncService.test.js +96 -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.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +132 -72
- package/dist/sync/workspacePullFeed.test.js +92 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +73 -6
- package/dist/sync/workspaceSyncModel.js +342 -73
- package/dist/sync/workspaceSyncModel.test.js +559 -14
- 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 +50 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- 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-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.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-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +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/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.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/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +8 -0
- package/dist/utils/assignees.js +10 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -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/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- 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 +9 -1
- package/dist/utils/taskActivity.js +290 -22
- package/dist/utils/taskActivity.test.js +141 -8
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- 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/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +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/TaskSettings-ln0aF7xc.js +0 -9
- 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
package/dist/mcp/runtime.test.js
CHANGED
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { createHash, randomBytes } from 'crypto';
|
|
5
6
|
import { createTaskforceMcpServer } from './runtime';
|
|
6
7
|
import { MCP_TOOL_REGISTRY } from './toolRegistry';
|
|
7
8
|
import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce';
|
|
@@ -17,6 +18,7 @@ function createRuntime(projectRoot, runtimeMode, options) {
|
|
|
17
18
|
? {
|
|
18
19
|
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
19
20
|
userId: options?.authUserId || 'owner-1',
|
|
21
|
+
tokenId: options?.authTokenId || null,
|
|
20
22
|
actorType: 'oauth',
|
|
21
23
|
aiProfileToken: options?.authAiProfileToken || null,
|
|
22
24
|
aiProfileName: options?.authAiProfileName || null,
|
|
@@ -160,6 +162,34 @@ function parseToolJson(result) {
|
|
|
160
162
|
expect(text).toBeTruthy();
|
|
161
163
|
return JSON.parse(String(text));
|
|
162
164
|
}
|
|
165
|
+
function createOauthGrantToken(core, userId, workspaceId) {
|
|
166
|
+
const redirectUri = 'https://chat.openai.com/aip/callback';
|
|
167
|
+
const client = core.registerMcpOAuthClient({
|
|
168
|
+
provider: 'chatgpt',
|
|
169
|
+
clientName: 'ChatGPT Connector',
|
|
170
|
+
redirectUris: [redirectUri],
|
|
171
|
+
});
|
|
172
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
173
|
+
const codeChallenge = createHash('sha256').update(verifier).digest('base64url');
|
|
174
|
+
const issued = core.issueMcpOAuthAuthorizationCode({
|
|
175
|
+
provider: 'chatgpt',
|
|
176
|
+
clientId: client.id,
|
|
177
|
+
userId,
|
|
178
|
+
workspaceId,
|
|
179
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
180
|
+
redirectUri,
|
|
181
|
+
codeChallenge,
|
|
182
|
+
codeChallengeMethod: 'S256',
|
|
183
|
+
});
|
|
184
|
+
const exchanged = core.exchangeMcpOAuthAuthorizationCode({
|
|
185
|
+
provider: 'chatgpt',
|
|
186
|
+
clientId: client.id,
|
|
187
|
+
code: issued.code,
|
|
188
|
+
redirectUri,
|
|
189
|
+
codeVerifier: verifier,
|
|
190
|
+
});
|
|
191
|
+
return exchanged.accessToken;
|
|
192
|
+
}
|
|
163
193
|
function seedAnnotatedAttachmentFixture(projectRoot) {
|
|
164
194
|
const core = new TaskforceCore({
|
|
165
195
|
projectRoot,
|
|
@@ -323,6 +353,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
323
353
|
const getTask = getTool(result, 'get_task');
|
|
324
354
|
const getPaths = getTool(result, 'get_paths');
|
|
325
355
|
const searchImages = getTool(result, 'search_images');
|
|
356
|
+
const getImage = getTool(result, 'get_image');
|
|
326
357
|
const getDocument = getTool(result, 'get_document');
|
|
327
358
|
const searchDocuments = getTool(result, 'search_documents');
|
|
328
359
|
const listAnnotatedAttachmentSessions = getTool(result, 'get_annotated_attachment_sessions');
|
|
@@ -341,10 +372,13 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
341
372
|
expect(getTask.description).toContain('Do not use this for image references like I-123 or document references like D-123.');
|
|
342
373
|
expect(getPaths.description).toContain('Get project and Taskforce storage paths.');
|
|
343
374
|
expect(searchImages.description).toContain('I-123 reference');
|
|
375
|
+
expect(getImage.description).toContain('placement, or visual correction feedback');
|
|
344
376
|
expect(getDocument.description).toContain('D-123');
|
|
345
377
|
expect(searchDocuments.description).toContain('D-123 reference');
|
|
346
378
|
expect(listAnnotatedAttachmentSessions.description).toContain('saved annotation sessions');
|
|
379
|
+
expect(listAnnotatedAttachmentSessions.description).toContain('inspect the saved session payload next');
|
|
347
380
|
expect(readAnnotatedAttachmentPayload.description).toContain('derived crop hints');
|
|
381
|
+
expect(readAnnotatedAttachmentPayload.description).toContain('authoritative instruction source');
|
|
348
382
|
expect(createAnnotatedAttachmentSession.description).toContain('before adding markers');
|
|
349
383
|
expect(deleteAnnotatedAttachmentSession.description).toContain('rather than deleting markers one by one');
|
|
350
384
|
expect(reviewImage.description).toContain('first-pass visual review');
|
|
@@ -381,6 +415,14 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
381
415
|
const getTaskAttachments = getTool(result, 'get_task_attachments');
|
|
382
416
|
const readTaskAttachment = getTool(result, 'read_task_attachment');
|
|
383
417
|
const deleteTaskAttachment = getTool(result, 'delete_task_attachment');
|
|
418
|
+
const getWorkstreamAttachments = getTool(result, 'get_workstream_attachments');
|
|
419
|
+
const readWorkstreamAttachment = getTool(result, 'read_workstream_attachment');
|
|
420
|
+
const saveWorkstreamAttachment = getTool(result, 'save_workstream_attachment');
|
|
421
|
+
const deleteWorkstreamAttachment = getTool(result, 'delete_workstream_attachment');
|
|
422
|
+
const getInitiativeAttachments = getTool(result, 'get_initiative_attachments');
|
|
423
|
+
const readInitiativeAttachment = getTool(result, 'read_initiative_attachment');
|
|
424
|
+
const saveInitiativeAttachment = getTool(result, 'save_initiative_attachment');
|
|
425
|
+
const deleteInitiativeAttachment = getTool(result, 'delete_initiative_attachment');
|
|
384
426
|
const deleteAnnotatedAttachmentSession = getTool(result, 'delete_annotated_attachment_session');
|
|
385
427
|
const archiveTask = getTool(result, 'archive_task');
|
|
386
428
|
const startTask = getTool(result, 'start_task');
|
|
@@ -399,9 +441,14 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
399
441
|
expect(getTask.description).toContain('Do not fetch attachment path URLs directly');
|
|
400
442
|
expect(getTask.description).toContain('Do not use this for image references like I-123 or document references like D-123.');
|
|
401
443
|
expect(searchImages.description).toContain('get_image');
|
|
444
|
+
expect(getImage.inputSchema?.properties?.id).toBeDefined();
|
|
445
|
+
expect(getImage.inputSchema?.properties?.imageId).toBeDefined();
|
|
402
446
|
expect(getImage.description).toContain('Use this first when the user references an image');
|
|
447
|
+
expect(getImage.description).toContain('read the saved annotation payload before making changes');
|
|
403
448
|
expect(searchDocuments.description).toContain('get_document');
|
|
404
449
|
expect(searchDocuments.description).toContain('read_document_content');
|
|
450
|
+
expect(getDocument.inputSchema?.properties?.id).toBeDefined();
|
|
451
|
+
expect(getDocument.inputSchema?.properties?.documentId).toBeDefined();
|
|
405
452
|
expect(getDocument.description).toContain('Use this first when the user references a document');
|
|
406
453
|
expect(getDocument.description).toContain('read_document_content');
|
|
407
454
|
expect(getCurrentProfile.description).toContain('current profile id');
|
|
@@ -417,9 +464,24 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
417
464
|
expect(deleteAnnotatedAttachmentSession.description).toContain('whole session was created by mistake');
|
|
418
465
|
expect(getTaskAttachments.description).toContain('use read_task_attachment for file contents');
|
|
419
466
|
expect(getTaskAttachments.description).toContain('prefer get_document and read_document_content');
|
|
467
|
+
expect(getTaskAttachments.inputSchema?.properties?.taskId).toBeDefined();
|
|
468
|
+
expect(saveTaskAttachment.description).toContain('staged remote uploads');
|
|
469
|
+
expect(saveTaskAttachment.inputSchema?.properties?.uploadedPath).toBeDefined();
|
|
470
|
+
expect(saveTaskAttachment.inputSchema?.properties?.uploadDraftId).toBeDefined();
|
|
471
|
+
expect(saveTaskAttachment.inputSchema?.properties?.mimeType).toBeDefined();
|
|
472
|
+
expect(saveTaskAttachment.inputSchema?.properties?.sizeBytes).toBeDefined();
|
|
473
|
+
expect(getTaskAttachments.inputSchema?.properties?.id).toBeDefined();
|
|
420
474
|
expect(readTaskAttachment.description).toContain('Prefer this tool over opening attachment path URLs directly');
|
|
421
475
|
expect(readTaskAttachment.description).toContain('canonical document');
|
|
422
476
|
expect(deleteTaskAttachment.description).toContain('Use get_task_attachments first');
|
|
477
|
+
expect(getWorkstreamAttachments.description).toContain('use read_workstream_attachment for file contents');
|
|
478
|
+
expect(readWorkstreamAttachment.description).toContain('Prefer this tool over opening attachment path URLs directly');
|
|
479
|
+
expect(saveWorkstreamAttachment.description).toContain('attach it to a workstream');
|
|
480
|
+
expect(deleteWorkstreamAttachment.description).toContain('Use get_workstream_attachments first');
|
|
481
|
+
expect(getInitiativeAttachments.description).toContain('use read_initiative_attachment for file contents');
|
|
482
|
+
expect(readInitiativeAttachment.description).toContain('Prefer this tool over opening attachment path URLs directly');
|
|
483
|
+
expect(saveInitiativeAttachment.description).toContain('attach it to an initiative');
|
|
484
|
+
expect(deleteInitiativeAttachment.description).toContain('Use get_initiative_attachments first');
|
|
423
485
|
expect(archiveTask.description).toContain('active tasks may still use status=done for final lookover');
|
|
424
486
|
expect(startTask.description).toContain('preferred AI start-of-task workflow');
|
|
425
487
|
expect(startTask.description).toContain('will not steal a task from a different assignee');
|
|
@@ -428,6 +490,8 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
428
490
|
expect(finishTask.description).toContain('outcome=\'review\' when human validation or approval is needed');
|
|
429
491
|
expect(finishTask.description).toContain('either finish it with this tool or explain why it remains open');
|
|
430
492
|
expect(finishTask.description).toContain('This tool never archives automatically');
|
|
493
|
+
expect(reviewDocument.inputSchema?.properties?.prompt).toBeDefined();
|
|
494
|
+
expect(reviewDocument.inputSchema?.properties?.comments).toBeDefined();
|
|
431
495
|
expect(updateTask.description).toContain('Use start_task when beginning active work on a tracked task');
|
|
432
496
|
expect(updateTask.description).toContain('prefer finish_task for the final completion or review handoff');
|
|
433
497
|
expect(assignTask.description).toContain('For assigning 3 or more tasks at once, use bulk_assign_tasks instead.');
|
|
@@ -441,21 +505,25 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
441
505
|
const saveTaskAttachmentSchema = saveTaskAttachment.inputSchema;
|
|
442
506
|
expect(saveTaskAttachmentSchema.properties.content.description).toContain('use this for AI-authored markdown/text attachments');
|
|
443
507
|
expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('approved agent workspace root');
|
|
508
|
+
expect(saveTaskAttachmentSchema.properties.profileToken.description).toContain('does not preserve session identity');
|
|
444
509
|
expect(resolveProfile.description).toContain('AI collaborator profile');
|
|
510
|
+
expect(resolveProfile.description).toContain('private agent/client state');
|
|
511
|
+
expect(resolveProfile.description).toContain('Never write profileToken values');
|
|
445
512
|
expect(resolveProfile.description).toContain('Update semantics');
|
|
446
513
|
expect(resolveProfile.description).toContain('empty strings are ignored rather than clearing');
|
|
447
514
|
expect(resolveProfile.inputSchema?.properties?.surfaceType).toBeDefined();
|
|
448
515
|
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('self-declared AI surface type');
|
|
449
516
|
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('agent for a persistent named AI collaborator in Taskforce');
|
|
450
|
-
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('
|
|
451
|
-
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('
|
|
517
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('cli for terminal-native coding agents');
|
|
518
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('coding_tool for coding surfaces that are not specifically CLI or IDE-native');
|
|
519
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('If you are unsure or none of those fit');
|
|
452
520
|
const surfaceTypeSchema = resolveProfile.inputSchema?.properties?.surfaceType;
|
|
453
521
|
expect(surfaceTypeSchema?.anyOf?.[0]?.enum).toEqual([
|
|
454
522
|
'chat_app',
|
|
455
523
|
'ide',
|
|
524
|
+
'cli',
|
|
456
525
|
'coding_tool',
|
|
457
|
-
'agent'
|
|
458
|
-
'other'
|
|
526
|
+
'agent'
|
|
459
527
|
]);
|
|
460
528
|
}
|
|
461
529
|
finally {
|
|
@@ -636,6 +704,32 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
636
704
|
})
|
|
637
705
|
}));
|
|
638
706
|
});
|
|
707
|
+
it('returns structured transient MCP timeout details for recoverable read failures', async () => {
|
|
708
|
+
const { buildRecoverableToolErrorResult } = await import('./runtime');
|
|
709
|
+
const result = buildRecoverableToolErrorResult(new Error('Postgres worker timed out while reading workstream attachments'), 'req-timeout-1');
|
|
710
|
+
const payload = parseToolJson(result);
|
|
711
|
+
expect(payload.success).toBe(false);
|
|
712
|
+
expect(payload.code).toBe('MCP_BACKEND_TIMEOUT');
|
|
713
|
+
expect(payload.kind).toBe('backend_timeout');
|
|
714
|
+
expect(payload.retryable).toBe(true);
|
|
715
|
+
expect(payload.requestId).toBe('req-timeout-1');
|
|
716
|
+
expect(payload.retryHint).toContain('backend');
|
|
717
|
+
expect(payload.details).toEqual(expect.objectContaining({
|
|
718
|
+
kind: 'backend_timeout',
|
|
719
|
+
retryable: true
|
|
720
|
+
}));
|
|
721
|
+
});
|
|
722
|
+
it('returns structured transient MCP transport details for recoverable transport failures', async () => {
|
|
723
|
+
const { buildRecoverableToolErrorResult } = await import('./runtime');
|
|
724
|
+
const result = buildRecoverableToolErrorResult(new Error('fetch failed'), 'req-fetch-1');
|
|
725
|
+
const payload = parseToolJson(result);
|
|
726
|
+
expect(payload.success).toBe(false);
|
|
727
|
+
expect(payload.code).toBe('MCP_TRANSPORT_FAILURE');
|
|
728
|
+
expect(payload.kind).toBe('transport_failure');
|
|
729
|
+
expect(payload.retryable).toBe(true);
|
|
730
|
+
expect(payload.requestId).toBe('req-fetch-1');
|
|
731
|
+
expect(payload.retryHint).toContain('transport');
|
|
732
|
+
});
|
|
639
733
|
it('blocks mutating MCP tools until resolve_profile is called', async () => {
|
|
640
734
|
const projectRoot = createProjectRoot();
|
|
641
735
|
try {
|
|
@@ -733,7 +827,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
733
827
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
734
828
|
}
|
|
735
829
|
});
|
|
736
|
-
it('
|
|
830
|
+
it('creates a separate cloud profile and persists metadata when a same-name local profile already exists', async () => {
|
|
737
831
|
const projectRoot = createProjectRoot();
|
|
738
832
|
try {
|
|
739
833
|
const initial = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
@@ -754,29 +848,36 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
754
848
|
authAiProfileSurfaceType: 'coding_tool'
|
|
755
849
|
}));
|
|
756
850
|
expect(currentProfile.profile).toEqual(expect.objectContaining({
|
|
757
|
-
id: initial.profile.id,
|
|
758
851
|
name: 'Automation Codex',
|
|
759
852
|
description: 'OpenAI Codex coding agent connected through MCP.',
|
|
760
853
|
role: 'Coding Agent',
|
|
761
854
|
provider: 'OpenAI',
|
|
762
855
|
model: 'Codex',
|
|
763
|
-
surfaceType: 'coding_tool'
|
|
856
|
+
surfaceType: 'coding_tool',
|
|
857
|
+
seatScope: 'cloud_metered'
|
|
764
858
|
}));
|
|
859
|
+
expect(currentProfile.profile.id).not.toBe(initial.profile.id);
|
|
765
860
|
const reloadedCore = new TaskforceCore({
|
|
766
861
|
projectRoot,
|
|
767
862
|
storagePath: path.join(projectRoot, '.taskforce')
|
|
768
863
|
});
|
|
769
|
-
expect(reloadedCore.listAiProfiles('workspace-1')).toEqual([
|
|
864
|
+
expect(reloadedCore.listAiProfiles('workspace-1')).toEqual(expect.arrayContaining([
|
|
770
865
|
expect.objectContaining({
|
|
771
866
|
id: initial.profile.id,
|
|
772
867
|
name: 'Automation Codex',
|
|
868
|
+
seatScope: 'local_unmetered'
|
|
869
|
+
}),
|
|
870
|
+
expect.objectContaining({
|
|
871
|
+
id: currentProfile.profile.id,
|
|
872
|
+
name: 'Automation Codex',
|
|
773
873
|
description: 'OpenAI Codex coding agent connected through MCP.',
|
|
774
874
|
role: 'Coding Agent',
|
|
775
875
|
provider: 'OpenAI',
|
|
776
876
|
model: 'Codex',
|
|
777
|
-
surfaceType: 'coding_tool'
|
|
877
|
+
surfaceType: 'coding_tool',
|
|
878
|
+
seatScope: 'cloud_metered'
|
|
778
879
|
})
|
|
779
|
-
]);
|
|
880
|
+
]));
|
|
780
881
|
reloadedCore.close();
|
|
781
882
|
}
|
|
782
883
|
finally {
|
|
@@ -801,17 +902,140 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
801
902
|
id: resolved.profile.id,
|
|
802
903
|
username: resolved.profile.username
|
|
803
904
|
}));
|
|
905
|
+
expect(created.createdByActor.meta).not.toHaveProperty('profileToken');
|
|
804
906
|
}
|
|
805
907
|
finally {
|
|
806
908
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
807
909
|
}
|
|
808
910
|
});
|
|
809
|
-
it('
|
|
911
|
+
it('persists resolved OAuth connector identity for later stateless writes', async () => {
|
|
912
|
+
const projectRoot = createProjectRoot();
|
|
913
|
+
try {
|
|
914
|
+
const seedCore = new TaskforceCore({
|
|
915
|
+
projectRoot,
|
|
916
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
917
|
+
});
|
|
918
|
+
Object.defineProperty(seedCore, 'getAccountAccessStatus', {
|
|
919
|
+
configurable: true,
|
|
920
|
+
value: () => ({
|
|
921
|
+
gate: 'ok',
|
|
922
|
+
hasEntitlement: true,
|
|
923
|
+
canAccessApp: true,
|
|
924
|
+
canAccessPlans: true,
|
|
925
|
+
planSelectionRequired: false,
|
|
926
|
+
message: null,
|
|
927
|
+
entitlement: null
|
|
928
|
+
})
|
|
929
|
+
});
|
|
930
|
+
const task = seedCore.createTask({ title: 'OAuth connector identity task' }, { workspaceId: 'workspace-1' });
|
|
931
|
+
const accessToken = createOauthGrantToken(seedCore, 'owner-1', 'workspace-1');
|
|
932
|
+
const initialAuth = seedCore.authenticateMcpOAuthAccessToken(accessToken);
|
|
933
|
+
expect(initialAuth?.grantId).toBeTruthy();
|
|
934
|
+
expect(initialAuth?.aiProfileToken).toBeFalsy();
|
|
935
|
+
seedCore.close();
|
|
936
|
+
const resolveRuntime = createRuntime(projectRoot, 'cloud', {
|
|
937
|
+
authTokenId: initialAuth.grantId,
|
|
938
|
+
});
|
|
939
|
+
const resolved = parseToolJson(await callToolWithRuntime(resolveRuntime, 'resolve_profile', {
|
|
940
|
+
name: 'Codex Connector',
|
|
941
|
+
icon: 'Code',
|
|
942
|
+
color: '#0ea5e9',
|
|
943
|
+
}, { resolveProfile: false }));
|
|
944
|
+
const authCore = new TaskforceCore({
|
|
945
|
+
projectRoot,
|
|
946
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
947
|
+
});
|
|
948
|
+
Object.defineProperty(authCore, 'getAccountAccessStatus', {
|
|
949
|
+
configurable: true,
|
|
950
|
+
value: () => ({
|
|
951
|
+
gate: 'ok',
|
|
952
|
+
hasEntitlement: true,
|
|
953
|
+
canAccessApp: true,
|
|
954
|
+
canAccessPlans: true,
|
|
955
|
+
planSelectionRequired: false,
|
|
956
|
+
message: null,
|
|
957
|
+
entitlement: null
|
|
958
|
+
})
|
|
959
|
+
});
|
|
960
|
+
const persistedAuth = authCore.authenticateMcpOAuthAccessToken(accessToken);
|
|
961
|
+
expect(persistedAuth).toEqual(expect.objectContaining({
|
|
962
|
+
grantId: initialAuth.grantId,
|
|
963
|
+
aiProfileId: resolved.profile.id,
|
|
964
|
+
aiProfileToken: resolved.profile.profileToken,
|
|
965
|
+
aiProfileName: 'Codex Connector',
|
|
966
|
+
}));
|
|
967
|
+
authCore.close();
|
|
968
|
+
const writeRuntime = createRuntime(projectRoot, 'cloud', {
|
|
969
|
+
authTokenId: persistedAuth.grantId,
|
|
970
|
+
authAiProfileToken: persistedAuth.aiProfileToken || undefined,
|
|
971
|
+
authAiProfileName: persistedAuth.aiProfileName || undefined,
|
|
972
|
+
});
|
|
973
|
+
const result = await callToolWithRuntime(writeRuntime, 'add_comment', {
|
|
974
|
+
id: task.id,
|
|
975
|
+
text: 'Stateless connector write after profile resolution',
|
|
976
|
+
}, { resolveProfile: false });
|
|
977
|
+
expect(result.isError).toBeFalsy();
|
|
978
|
+
expect(String(result.content?.[0]?.text || '')).toContain('Comment added');
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
it('allows save_task_attachment in a fresh runtime when profileToken is passed', async () => {
|
|
810
985
|
const projectRoot = createProjectRoot();
|
|
811
986
|
try {
|
|
987
|
+
const core = new TaskforceCore({
|
|
988
|
+
projectRoot,
|
|
989
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
990
|
+
});
|
|
991
|
+
const task = core.createTask({
|
|
992
|
+
title: 'Stateless attachment token write'
|
|
993
|
+
}, {
|
|
994
|
+
workspaceId: 'workspace-1'
|
|
995
|
+
});
|
|
996
|
+
core.close();
|
|
812
997
|
const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
813
|
-
name: '
|
|
998
|
+
name: 'Codex',
|
|
999
|
+
icon: 'Bot',
|
|
1000
|
+
color: '#ef6c00'
|
|
814
1001
|
}, { resolveProfile: false }));
|
|
1002
|
+
const result = await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1003
|
+
id: task.referenceLabel,
|
|
1004
|
+
filename: 'stateless-note.txt',
|
|
1005
|
+
content: 'hello from a stateless attachment write',
|
|
1006
|
+
profileToken: resolved.profile.profileToken,
|
|
1007
|
+
}, { resolveProfile: false });
|
|
1008
|
+
expect(result.isError).not.toBe(true);
|
|
1009
|
+
const after = new TaskforceCore({
|
|
1010
|
+
projectRoot,
|
|
1011
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1012
|
+
});
|
|
1013
|
+
try {
|
|
1014
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1015
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1016
|
+
const attachment = updated?.attachments?.[0];
|
|
1017
|
+
expect(attachment?.assetId).toMatch(/^asset-/);
|
|
1018
|
+
const store = new WorkspaceAssetStore(after.getDatabaseAdapter(), after.getTenantId());
|
|
1019
|
+
const asset = store.get(String(attachment?.assetId || ''), 'workspace-1');
|
|
1020
|
+
expect(asset?.createdByUserId).toBe(resolved.profile.id);
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
after.close();
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
finally {
|
|
1027
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
it('allows mutating cloud MCP tools when the connection is configured with an AI profile token', async () => {
|
|
1031
|
+
const projectRoot = createProjectRoot();
|
|
1032
|
+
try {
|
|
1033
|
+
const resolved = parseToolJson(await callTool(projectRoot, 'cloud', 'resolve_profile', {
|
|
1034
|
+
name: 'Automation Codex'
|
|
1035
|
+
}, {
|
|
1036
|
+
workspaceId: 'workspace-1',
|
|
1037
|
+
resolveProfile: false
|
|
1038
|
+
}));
|
|
815
1039
|
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
816
1040
|
title: 'Configured token write'
|
|
817
1041
|
}, {
|
|
@@ -866,6 +1090,100 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
866
1090
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
867
1091
|
}
|
|
868
1092
|
});
|
|
1093
|
+
it('blocks cloud auto-bind when the configured AI profile has been retired by name until resolve_profile explicitly reactivates it', async () => {
|
|
1094
|
+
const projectRoot = createProjectRoot();
|
|
1095
|
+
try {
|
|
1096
|
+
const core = new TaskforceCore({
|
|
1097
|
+
projectRoot,
|
|
1098
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1099
|
+
});
|
|
1100
|
+
const resolved = core.resolveAiProfile({
|
|
1101
|
+
workspaceId: 'workspace-1',
|
|
1102
|
+
name: 'Automation Codex',
|
|
1103
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1104
|
+
core.archiveAiProfile({
|
|
1105
|
+
workspaceId: 'workspace-1',
|
|
1106
|
+
profileId: resolved.profile.id,
|
|
1107
|
+
archivedBy: 'owner-1',
|
|
1108
|
+
reason: 'manual_archive'
|
|
1109
|
+
});
|
|
1110
|
+
core.close();
|
|
1111
|
+
const blocked = await callTool(projectRoot, 'cloud', 'create_task', {
|
|
1112
|
+
title: 'Should stay retired'
|
|
1113
|
+
}, {
|
|
1114
|
+
workspaceId: 'workspace-1',
|
|
1115
|
+
resolveProfile: false,
|
|
1116
|
+
authAiProfileName: 'Automation Codex'
|
|
1117
|
+
});
|
|
1118
|
+
expect(blocked.isError).toBe(true);
|
|
1119
|
+
const blockedPayload = parseToolJson(blocked);
|
|
1120
|
+
expect(blockedPayload.code).toBe('AI_PROFILE_RETIRED');
|
|
1121
|
+
expect(blockedPayload.error).toContain('has been retired from this workspace');
|
|
1122
|
+
expect(blockedPayload.nextTool).toBe('resolve_profile');
|
|
1123
|
+
const reactivated = parseToolJson(await callTool(projectRoot, 'cloud', 'resolve_profile', {
|
|
1124
|
+
name: 'Automation Codex',
|
|
1125
|
+
surfaceType: 'agent'
|
|
1126
|
+
}, {
|
|
1127
|
+
workspaceId: 'workspace-1',
|
|
1128
|
+
resolveProfile: false,
|
|
1129
|
+
authAiProfileName: 'Automation Codex'
|
|
1130
|
+
}));
|
|
1131
|
+
expect(reactivated.created).toBe(false);
|
|
1132
|
+
expect(reactivated.profile.id).toBe(resolved.profile.id);
|
|
1133
|
+
expect(reactivated.profile.archivedAt ?? null).toBeNull();
|
|
1134
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
1135
|
+
title: 'Reactivated automation write'
|
|
1136
|
+
}, {
|
|
1137
|
+
workspaceId: 'workspace-1',
|
|
1138
|
+
resolveProfile: false,
|
|
1139
|
+
authAiProfileName: 'Automation Codex'
|
|
1140
|
+
}));
|
|
1141
|
+
expect(created.title).toBe('Reactivated automation write');
|
|
1142
|
+
expect(created.createdBy).toBe(resolved.profile.id);
|
|
1143
|
+
}
|
|
1144
|
+
finally {
|
|
1145
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
it('blocks cloud auto-bind when the configured AI profile token belongs to a retired profile', async () => {
|
|
1149
|
+
const projectRoot = createProjectRoot();
|
|
1150
|
+
try {
|
|
1151
|
+
const core = new TaskforceCore({
|
|
1152
|
+
projectRoot,
|
|
1153
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1154
|
+
});
|
|
1155
|
+
const resolved = core.resolveAiProfile({
|
|
1156
|
+
workspaceId: 'workspace-1',
|
|
1157
|
+
name: 'Automation Codex',
|
|
1158
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1159
|
+
core.archiveAiProfile({
|
|
1160
|
+
workspaceId: 'workspace-1',
|
|
1161
|
+
profileId: resolved.profile.id,
|
|
1162
|
+
archivedBy: 'owner-1',
|
|
1163
|
+
reason: 'manual_archive'
|
|
1164
|
+
});
|
|
1165
|
+
core.close();
|
|
1166
|
+
const blocked = await callTool(projectRoot, 'cloud', 'create_task', {
|
|
1167
|
+
title: 'Token should stay retired'
|
|
1168
|
+
}, {
|
|
1169
|
+
workspaceId: 'workspace-1',
|
|
1170
|
+
resolveProfile: false,
|
|
1171
|
+
authAiProfileToken: resolved.profile.profileToken,
|
|
1172
|
+
authAiProfileName: 'Automation Codex'
|
|
1173
|
+
});
|
|
1174
|
+
expect(blocked.isError).toBe(true);
|
|
1175
|
+
const blockedPayload = parseToolJson(blocked);
|
|
1176
|
+
expect(blockedPayload.code).toBe('AI_PROFILE_RETIRED');
|
|
1177
|
+
expect(blockedPayload.details).toEqual(expect.objectContaining({
|
|
1178
|
+
profileId: resolved.profile.id,
|
|
1179
|
+
profileToken: resolved.profile.profileToken,
|
|
1180
|
+
archivedReason: 'manual_archive'
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
finally {
|
|
1184
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
869
1187
|
it('blocks cloud auto-bind when the workspace has no remaining ai profile seats', async () => {
|
|
870
1188
|
const projectRoot = createProjectRoot();
|
|
871
1189
|
try {
|
|
@@ -895,8 +1213,8 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
895
1213
|
});
|
|
896
1214
|
core.resolveAiProfile({
|
|
897
1215
|
workspaceId: 'workspace-1',
|
|
898
|
-
name: 'Existing Automation Bot'
|
|
899
|
-
});
|
|
1216
|
+
name: 'Existing Automation Bot',
|
|
1217
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
900
1218
|
core.close();
|
|
901
1219
|
const result = await callTool(projectRoot, 'cloud', 'create_task', {
|
|
902
1220
|
title: 'Blocked automation write'
|
|
@@ -1009,8 +1327,8 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1009
1327
|
});
|
|
1010
1328
|
core.resolveAiProfile({
|
|
1011
1329
|
workspaceId: 'workspace-1',
|
|
1012
|
-
name: 'Existing Automation Bot'
|
|
1013
|
-
});
|
|
1330
|
+
name: 'Existing Automation Bot',
|
|
1331
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1014
1332
|
core.close();
|
|
1015
1333
|
const result = await callTool(projectRoot, 'cloud', 'create_task', {
|
|
1016
1334
|
title: 'Recovered per-call token write',
|
|
@@ -1035,112 +1353,435 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1035
1353
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1036
1354
|
}
|
|
1037
1355
|
});
|
|
1038
|
-
it('
|
|
1356
|
+
it('keeps local MCP profile resolution unmetered even when cloud seats are full', async () => {
|
|
1039
1357
|
const projectRoot = createProjectRoot();
|
|
1040
1358
|
try {
|
|
1041
1359
|
const core = new TaskforceCore({
|
|
1042
1360
|
projectRoot,
|
|
1043
1361
|
storagePath: path.join(projectRoot, '.taskforce')
|
|
1044
1362
|
});
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1363
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
1364
|
+
core.ensureBootstrapUserAccess({
|
|
1365
|
+
userId: 'owner-u1',
|
|
1366
|
+
workspaceId: 'workspace-1',
|
|
1367
|
+
role: 'owner',
|
|
1368
|
+
email: 'owner@example.com'
|
|
1369
|
+
});
|
|
1370
|
+
core.updatePlanVersionFeatures({
|
|
1371
|
+
planVersionId: 'free-v1',
|
|
1372
|
+
features: [
|
|
1373
|
+
{ featureKey: 'collaboration.ai_profiles', access: 'limited', config: { maxAiProfiles: 1 } }
|
|
1374
|
+
]
|
|
1375
|
+
});
|
|
1376
|
+
core.updateAccountEntitlement({
|
|
1377
|
+
accountId: 'owner-u1',
|
|
1378
|
+
planVersionId: 'free-v1',
|
|
1379
|
+
entitlementState: 'active',
|
|
1380
|
+
actorUserId: 'system-admin',
|
|
1381
|
+
actorRole: 'system_admin'
|
|
1382
|
+
});
|
|
1383
|
+
core.resolveAiProfile({
|
|
1384
|
+
workspaceId: 'workspace-1',
|
|
1385
|
+
name: 'Existing Automation Bot',
|
|
1386
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1050
1387
|
core.close();
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1388
|
+
const payload = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
1389
|
+
name: 'Local Codex'
|
|
1390
|
+
}, {
|
|
1391
|
+
workspaceId: 'workspace-1',
|
|
1392
|
+
resolveProfile: false
|
|
1053
1393
|
}));
|
|
1054
|
-
expect(
|
|
1055
|
-
expect(
|
|
1056
|
-
expect(
|
|
1057
|
-
expect(result.previousAssignee).toBe('unassigned');
|
|
1058
|
-
expect(result.previousStatus).toBe('task');
|
|
1059
|
-
expect(result.task.status).toBe('in-progress');
|
|
1060
|
-
expect(result.task.assignee).toMatch(/^ai-profile-/);
|
|
1394
|
+
expect(payload.created).toBe(true);
|
|
1395
|
+
expect(payload.profile.name).toBe('Local Codex');
|
|
1396
|
+
expect(payload.profile.seatScope).toBe('local_unmetered');
|
|
1061
1397
|
}
|
|
1062
1398
|
finally {
|
|
1063
1399
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1064
1400
|
}
|
|
1065
1401
|
});
|
|
1066
|
-
it('
|
|
1402
|
+
it('derives profile seat scope from the MCP runtime instead of auth metadata', async () => {
|
|
1067
1403
|
const projectRoot = createProjectRoot();
|
|
1068
1404
|
try {
|
|
1069
|
-
const
|
|
1405
|
+
const localRuntime = createTaskforceMcpServer({
|
|
1406
|
+
projectRoot,
|
|
1407
|
+
runtimeMode: 'local',
|
|
1408
|
+
workspaceId: 'workspace-1',
|
|
1409
|
+
logStartup: false,
|
|
1410
|
+
auth: {
|
|
1411
|
+
aiProfileSeatScope: 'cloud_metered',
|
|
1412
|
+
aiProfileName: 'Codex',
|
|
1413
|
+
aiProfileRole: 'Implementation',
|
|
1414
|
+
aiProfileProvider: 'OpenAI',
|
|
1415
|
+
aiProfileModel: 'gpt-5',
|
|
1416
|
+
aiProfileSurfaceType: 'coding_tool',
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
const localPayload = parseToolJson(await callToolWithRuntime(localRuntime, 'resolve_profile', {
|
|
1070
1420
|
name: 'Codex',
|
|
1071
|
-
|
|
1072
|
-
|
|
1421
|
+
surfaceType: 'coding_tool',
|
|
1422
|
+
}, {
|
|
1423
|
+
resolveProfile: false
|
|
1073
1424
|
}));
|
|
1074
|
-
|
|
1425
|
+
expect(localPayload.profile.seatScope).toBe('local_unmetered');
|
|
1426
|
+
const cloudRuntime = createTaskforceMcpServer({
|
|
1075
1427
|
projectRoot,
|
|
1076
|
-
|
|
1428
|
+
runtimeMode: 'cloud',
|
|
1429
|
+
workspaceId: 'workspace-1',
|
|
1430
|
+
logStartup: false,
|
|
1431
|
+
auth: {
|
|
1432
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
1433
|
+
userId: 'owner-1',
|
|
1434
|
+
actorType: 'oauth',
|
|
1435
|
+
aiProfileSeatScope: 'local_unmetered',
|
|
1436
|
+
aiProfileName: 'Cloud Codex',
|
|
1437
|
+
aiProfileSurfaceType: 'coding_tool',
|
|
1438
|
+
}
|
|
1077
1439
|
});
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
core.close();
|
|
1084
|
-
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
1085
|
-
id: seeded.referenceLabel
|
|
1440
|
+
const cloudPayload = parseToolJson(await callToolWithRuntime(cloudRuntime, 'resolve_profile', {
|
|
1441
|
+
name: 'Cloud Codex',
|
|
1442
|
+
surfaceType: 'coding_tool',
|
|
1443
|
+
}, {
|
|
1444
|
+
resolveProfile: false
|
|
1086
1445
|
}));
|
|
1087
|
-
expect(
|
|
1088
|
-
expect(result.claimed).toBe(false);
|
|
1089
|
-
expect(result.statusChanged).toBe(true);
|
|
1090
|
-
expect(result.previousAssignee).toBe(profile.profile.id);
|
|
1091
|
-
expect(result.task.assignee).toBe(profile.profile.id);
|
|
1092
|
-
expect(result.task.status).toBe('in-progress');
|
|
1446
|
+
expect(cloudPayload.profile.seatScope).toBe('cloud_metered');
|
|
1093
1447
|
}
|
|
1094
1448
|
finally {
|
|
1095
1449
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1096
1450
|
}
|
|
1097
1451
|
});
|
|
1098
|
-
it('
|
|
1452
|
+
it('creates a separate local MCP profile when a cloud profile with the same name already exists', async () => {
|
|
1099
1453
|
const projectRoot = createProjectRoot();
|
|
1100
1454
|
try {
|
|
1101
1455
|
const core = new TaskforceCore({
|
|
1102
1456
|
projectRoot,
|
|
1103
1457
|
storagePath: path.join(projectRoot, '.taskforce')
|
|
1104
1458
|
});
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}, {
|
|
1459
|
+
const cloud = core.resolveAiProfile({
|
|
1460
|
+
workspaceId: 'workspace-1',
|
|
1461
|
+
name: 'Codex',
|
|
1462
|
+
surfaceType: 'coding_tool'
|
|
1463
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1110
1464
|
core.close();
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1465
|
+
const payload = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
1466
|
+
name: 'Codex',
|
|
1467
|
+
surfaceType: 'coding_tool'
|
|
1468
|
+
}, {
|
|
1469
|
+
workspaceId: 'workspace-1',
|
|
1470
|
+
resolveProfile: false
|
|
1471
|
+
}));
|
|
1472
|
+
expect(payload.created).toBe(true);
|
|
1473
|
+
expect(payload.profile.id).not.toBe(cloud.profile.id);
|
|
1474
|
+
expect(payload.profile.seatScope).toBe('local_unmetered');
|
|
1116
1475
|
}
|
|
1117
1476
|
finally {
|
|
1118
1477
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1119
1478
|
}
|
|
1120
1479
|
});
|
|
1121
|
-
it('
|
|
1480
|
+
it('attributes system-authored MCP comments to the current AI profile', async () => {
|
|
1122
1481
|
const projectRoot = createProjectRoot();
|
|
1123
1482
|
try {
|
|
1124
|
-
const
|
|
1483
|
+
const runtime = createTaskforceMcpServer({
|
|
1125
1484
|
projectRoot,
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
title: 'Start helper terminal refusal',
|
|
1130
|
-
status: 'done',
|
|
1131
|
-
assignee: 'unassigned'
|
|
1132
|
-
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1133
|
-
core.close();
|
|
1134
|
-
const result = await callTool(projectRoot, 'local', 'start_task', {
|
|
1135
|
-
id: seeded.referenceLabel
|
|
1485
|
+
runtimeMode: 'local',
|
|
1486
|
+
workspaceId: 'workspace-1',
|
|
1487
|
+
logStartup: false,
|
|
1136
1488
|
});
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1489
|
+
const profilePayload = parseToolJson(await callToolWithRuntime(runtime, 'resolve_profile', {
|
|
1490
|
+
name: 'Codex',
|
|
1491
|
+
icon: 'Bot',
|
|
1492
|
+
surfaceType: 'coding_tool',
|
|
1493
|
+
}, {
|
|
1494
|
+
resolveProfile: false,
|
|
1495
|
+
}));
|
|
1496
|
+
const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
1497
|
+
title: 'Comment attribution task',
|
|
1498
|
+
}));
|
|
1499
|
+
await callToolWithRuntime(runtime, 'add_comment', {
|
|
1500
|
+
id: created.referenceLabel,
|
|
1501
|
+
text: 'Completed the work.',
|
|
1502
|
+
author: 'system',
|
|
1503
|
+
});
|
|
1504
|
+
const task = parseToolJson(await callToolWithRuntime(runtime, 'get_task', {
|
|
1505
|
+
id: created.referenceLabel,
|
|
1506
|
+
}));
|
|
1507
|
+
const comment = task.comments.find((entry) => entry.text === 'Completed the work.');
|
|
1508
|
+
const event = task.taskEvents.find((entry) => entry.details?.commentId === comment?.id);
|
|
1509
|
+
expect(comment.author).toBe(profilePayload.profile.id);
|
|
1510
|
+
expect(comment.submittedAuthor).toBe(profilePayload.profile.id);
|
|
1511
|
+
expect(comment.actor.label).toBe('Codex');
|
|
1512
|
+
expect(event.actor).toBe(profilePayload.profile.id);
|
|
1513
|
+
expect(event.actorType).toBe('ai');
|
|
1514
|
+
expect(event.actorProfile.label).toBe('Codex');
|
|
1515
|
+
}
|
|
1516
|
+
finally {
|
|
1517
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
it('canonicalizes ambiguous AI display-name comment authors back to the bound profile', async () => {
|
|
1521
|
+
const projectRoot = createProjectRoot();
|
|
1522
|
+
try {
|
|
1523
|
+
const seedCore = new TaskforceCore({
|
|
1524
|
+
projectRoot,
|
|
1525
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1526
|
+
});
|
|
1527
|
+
seedCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
|
|
1528
|
+
seedCore.close();
|
|
1529
|
+
const runtime = createTaskforceMcpServer({
|
|
1530
|
+
projectRoot,
|
|
1531
|
+
runtimeMode: 'local',
|
|
1532
|
+
workspaceId: 'workspace-1',
|
|
1533
|
+
logStartup: false,
|
|
1534
|
+
});
|
|
1535
|
+
const profilePayload = parseToolJson(await callToolWithRuntime(runtime, 'resolve_profile', {
|
|
1536
|
+
name: 'Codex',
|
|
1537
|
+
icon: 'Bot',
|
|
1538
|
+
surfaceType: 'coding_tool',
|
|
1539
|
+
}, {
|
|
1540
|
+
resolveProfile: false,
|
|
1541
|
+
}));
|
|
1542
|
+
const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
1543
|
+
title: 'Ambiguous comment attribution task',
|
|
1544
|
+
}));
|
|
1545
|
+
await callToolWithRuntime(runtime, 'add_comment', {
|
|
1546
|
+
id: created.referenceLabel,
|
|
1547
|
+
text: 'Bound actor should win.',
|
|
1548
|
+
author: 'Codex',
|
|
1549
|
+
});
|
|
1550
|
+
const task = parseToolJson(await callToolWithRuntime(runtime, 'get_task', {
|
|
1551
|
+
id: created.referenceLabel,
|
|
1552
|
+
}));
|
|
1553
|
+
const comment = task.comments.find((entry) => entry.text === 'Bound actor should win.');
|
|
1554
|
+
const event = task.taskEvents.find((entry) => entry.details?.commentId === comment?.id);
|
|
1555
|
+
expect(comment.author).toBe(profilePayload.profile.id);
|
|
1556
|
+
expect(comment.submittedAuthor).toBe(profilePayload.profile.id);
|
|
1557
|
+
expect(comment.actor.id).toBe(profilePayload.profile.id);
|
|
1558
|
+
expect(event.actor).toBe(profilePayload.profile.id);
|
|
1559
|
+
expect(event.actorType).toBe('ai');
|
|
1560
|
+
}
|
|
1561
|
+
finally {
|
|
1562
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
it('preserves explicit human comment overrides for MCP writes', async () => {
|
|
1566
|
+
const projectRoot = createProjectRoot();
|
|
1567
|
+
try {
|
|
1568
|
+
const runtime = createTaskforceMcpServer({
|
|
1569
|
+
projectRoot,
|
|
1570
|
+
runtimeMode: 'local',
|
|
1571
|
+
workspaceId: 'workspace-1',
|
|
1572
|
+
logStartup: false,
|
|
1573
|
+
});
|
|
1574
|
+
const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
1575
|
+
title: 'Human comment override task',
|
|
1576
|
+
}));
|
|
1577
|
+
await callToolWithRuntime(runtime, 'add_comment', {
|
|
1578
|
+
id: created.referenceLabel,
|
|
1579
|
+
text: 'Human-authored note.',
|
|
1580
|
+
author: 'user',
|
|
1581
|
+
});
|
|
1582
|
+
const task = parseToolJson(await callToolWithRuntime(runtime, 'get_task', {
|
|
1583
|
+
id: created.referenceLabel,
|
|
1584
|
+
}));
|
|
1585
|
+
const comment = task.comments.find((entry) => entry.text === 'Human-authored note.');
|
|
1586
|
+
const event = task.taskEvents.find((entry) => entry.details?.commentId === comment?.id);
|
|
1587
|
+
expect(comment.author).toBe('user');
|
|
1588
|
+
expect(comment.submittedAuthor).toBe('user');
|
|
1589
|
+
expect(comment.actor.id).toBe('user');
|
|
1590
|
+
expect(event.actor).toBe('user');
|
|
1591
|
+
expect(event.actorType).toBe('human');
|
|
1592
|
+
}
|
|
1593
|
+
finally {
|
|
1594
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
it('ignores mutable MCP comment author aliases when an AI profile is bound', async () => {
|
|
1598
|
+
const projectRoot = createProjectRoot();
|
|
1599
|
+
try {
|
|
1600
|
+
const runtime = createTaskforceMcpServer({
|
|
1601
|
+
projectRoot,
|
|
1602
|
+
runtimeMode: 'local',
|
|
1603
|
+
workspaceId: 'workspace-1',
|
|
1604
|
+
logStartup: false,
|
|
1605
|
+
});
|
|
1606
|
+
const profilePayload = parseToolJson(await callToolWithRuntime(runtime, 'resolve_profile', {
|
|
1607
|
+
name: 'Codex',
|
|
1608
|
+
icon: 'Bot',
|
|
1609
|
+
surfaceType: 'coding_tool',
|
|
1610
|
+
}, {
|
|
1611
|
+
resolveProfile: false,
|
|
1612
|
+
}));
|
|
1613
|
+
const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
1614
|
+
title: 'Strict comment attribution task',
|
|
1615
|
+
}));
|
|
1616
|
+
await callToolWithRuntime(runtime, 'add_comment', {
|
|
1617
|
+
id: created.referenceLabel,
|
|
1618
|
+
text: 'Alias should not win over the bound profile.',
|
|
1619
|
+
author: 'Claude',
|
|
1620
|
+
});
|
|
1621
|
+
await callToolWithRuntime(runtime, 'add_comment', {
|
|
1622
|
+
id: created.referenceLabel,
|
|
1623
|
+
text: 'Explicit human override stays human.',
|
|
1624
|
+
author: 'user',
|
|
1625
|
+
});
|
|
1626
|
+
const task = parseToolJson(await callToolWithRuntime(runtime, 'get_task', {
|
|
1627
|
+
id: created.referenceLabel,
|
|
1628
|
+
}));
|
|
1629
|
+
const aliasComment = task.comments.find((entry) => entry.text === 'Alias should not win over the bound profile.');
|
|
1630
|
+
const humanComment = task.comments.find((entry) => entry.text === 'Explicit human override stays human.');
|
|
1631
|
+
expect(aliasComment.author).toBe(profilePayload.profile.id);
|
|
1632
|
+
expect(aliasComment.submittedAuthor).toBe(profilePayload.profile.id);
|
|
1633
|
+
expect(aliasComment.actor.label).toBe('Codex');
|
|
1634
|
+
expect(humanComment.author).toBe('user');
|
|
1635
|
+
expect(humanComment.submittedAuthor).toBe('user');
|
|
1636
|
+
expect(humanComment.actor.kind).toBe('human');
|
|
1637
|
+
}
|
|
1638
|
+
finally {
|
|
1639
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
it('claims unassigned tasks and marks them in progress through start_task', async () => {
|
|
1643
|
+
const projectRoot = createProjectRoot();
|
|
1644
|
+
try {
|
|
1645
|
+
const core = new TaskforceCore({
|
|
1646
|
+
projectRoot,
|
|
1647
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1648
|
+
});
|
|
1649
|
+
const seeded = core.createTask({
|
|
1650
|
+
title: 'Start helper unassigned path',
|
|
1651
|
+
status: 'task',
|
|
1652
|
+
assignee: 'unassigned'
|
|
1653
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1654
|
+
core.close();
|
|
1655
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
1656
|
+
id: seeded.referenceLabel
|
|
1657
|
+
}));
|
|
1658
|
+
expect(result.started).toBe(true);
|
|
1659
|
+
expect(result.claimed).toBe(true);
|
|
1660
|
+
expect(result.statusChanged).toBe(true);
|
|
1661
|
+
expect(result.previousAssignee).toBe('unassigned');
|
|
1662
|
+
expect(result.previousStatus).toBe('task');
|
|
1663
|
+
expect(result.task.status).toBe('in-progress');
|
|
1664
|
+
expect(result.task.assignee).toMatch(/^ai-profile-/);
|
|
1665
|
+
}
|
|
1666
|
+
finally {
|
|
1667
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
it('sets tasks already assigned to the current AI in progress without stealing ownership', async () => {
|
|
1671
|
+
const projectRoot = createProjectRoot();
|
|
1672
|
+
try {
|
|
1673
|
+
const profile = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
1674
|
+
name: 'Codex',
|
|
1675
|
+
icon: 'Bot',
|
|
1676
|
+
color: '#ef6c00'
|
|
1677
|
+
}));
|
|
1678
|
+
const core = new TaskforceCore({
|
|
1679
|
+
projectRoot,
|
|
1680
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1681
|
+
});
|
|
1682
|
+
const seeded = core.createTask({
|
|
1683
|
+
title: 'Start helper owned path',
|
|
1684
|
+
status: 'task',
|
|
1685
|
+
assignee: profile.profile.id
|
|
1686
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1687
|
+
core.close();
|
|
1688
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
1689
|
+
id: seeded.referenceLabel
|
|
1690
|
+
}));
|
|
1691
|
+
expect(result.started).toBe(true);
|
|
1692
|
+
expect(result.claimed).toBe(false);
|
|
1693
|
+
expect(result.statusChanged).toBe(true);
|
|
1694
|
+
expect(result.previousAssignee).toBe(profile.profile.id);
|
|
1695
|
+
expect(result.task.assignee).toBe(profile.profile.id);
|
|
1696
|
+
expect(result.task.status).toBe('in-progress');
|
|
1697
|
+
}
|
|
1698
|
+
finally {
|
|
1699
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
it('allows review tasks to resume through start_task', async () => {
|
|
1703
|
+
const projectRoot = createProjectRoot();
|
|
1704
|
+
try {
|
|
1705
|
+
const core = new TaskforceCore({
|
|
1706
|
+
projectRoot,
|
|
1707
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1708
|
+
});
|
|
1709
|
+
const seeded = core.createTask({
|
|
1710
|
+
title: 'Start helper review resume',
|
|
1711
|
+
status: 'review',
|
|
1712
|
+
assignee: 'unassigned'
|
|
1713
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1714
|
+
core.close();
|
|
1715
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
1716
|
+
id: seeded.referenceLabel
|
|
1717
|
+
}));
|
|
1718
|
+
expect(result.started).toBe(true);
|
|
1719
|
+
expect(result.claimed).toBe(true);
|
|
1720
|
+
expect(result.statusChanged).toBe(true);
|
|
1721
|
+
expect(result.previousStatus).toBe('review');
|
|
1722
|
+
expect(result.task.status).toBe('in-progress');
|
|
1723
|
+
expect(result.task.assignee).toMatch(/^ai-profile-/);
|
|
1724
|
+
}
|
|
1725
|
+
finally {
|
|
1726
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
it('refuses to steal tasks already assigned to someone else through start_task', async () => {
|
|
1730
|
+
const projectRoot = createProjectRoot();
|
|
1731
|
+
try {
|
|
1732
|
+
const core = new TaskforceCore({
|
|
1733
|
+
projectRoot,
|
|
1734
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1735
|
+
});
|
|
1736
|
+
const seeded = core.createTask({
|
|
1737
|
+
title: 'Start helper assignee refusal',
|
|
1738
|
+
status: 'task',
|
|
1739
|
+
assignee: 'owner-1'
|
|
1740
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1741
|
+
core.close();
|
|
1742
|
+
const result = await callTool(projectRoot, 'local', 'start_task', {
|
|
1743
|
+
id: seeded.referenceLabel
|
|
1744
|
+
});
|
|
1745
|
+
expect(result.isError).toBe(true);
|
|
1746
|
+
expect(result.content?.[0]?.text).toContain('will not steal work from another assignee');
|
|
1747
|
+
}
|
|
1748
|
+
finally {
|
|
1749
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
it('rejects done and cancelled tasks through start_task', async () => {
|
|
1753
|
+
const projectRoot = createProjectRoot();
|
|
1754
|
+
try {
|
|
1755
|
+
const core = new TaskforceCore({
|
|
1756
|
+
projectRoot,
|
|
1757
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1758
|
+
});
|
|
1759
|
+
const doneTask = core.createTask({
|
|
1760
|
+
title: 'Start helper done refusal',
|
|
1761
|
+
status: 'done',
|
|
1762
|
+
assignee: 'unassigned'
|
|
1763
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1764
|
+
const cancelledTask = core.createTask({
|
|
1765
|
+
title: 'Start helper cancelled refusal',
|
|
1766
|
+
status: 'cancelled',
|
|
1767
|
+
assignee: 'unassigned'
|
|
1768
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1769
|
+
core.close();
|
|
1770
|
+
const doneResult = await callTool(projectRoot, 'local', 'start_task', {
|
|
1771
|
+
id: doneTask.referenceLabel
|
|
1772
|
+
});
|
|
1773
|
+
const cancelledResult = await callTool(projectRoot, 'local', 'start_task', {
|
|
1774
|
+
id: cancelledTask.referenceLabel
|
|
1775
|
+
});
|
|
1776
|
+
expect(doneResult.isError).toBe(true);
|
|
1777
|
+
expect(doneResult.content?.[0]?.text).toContain('must be reopened explicitly');
|
|
1778
|
+
expect(cancelledResult.isError).toBe(true);
|
|
1779
|
+
expect(cancelledResult.content?.[0]?.text).toContain('must be reopened explicitly');
|
|
1780
|
+
}
|
|
1781
|
+
finally {
|
|
1782
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1144
1785
|
it('requires AI identity for start_task when bootstrap is disabled', async () => {
|
|
1145
1786
|
const projectRoot = createProjectRoot();
|
|
1146
1787
|
try {
|
|
@@ -1372,6 +2013,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1372
2013
|
isCurrentProfile: true
|
|
1373
2014
|
})
|
|
1374
2015
|
]));
|
|
2016
|
+
expect(JSON.stringify(payload.assignees)).not.toContain(resolved.profile.profileToken);
|
|
1375
2017
|
}
|
|
1376
2018
|
finally {
|
|
1377
2019
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -1483,6 +2125,50 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1483
2125
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1484
2126
|
}
|
|
1485
2127
|
});
|
|
2128
|
+
it('auto-binds a configured cloud AI name to an existing profile when the token is stale', async () => {
|
|
2129
|
+
const projectRoot = createProjectRoot();
|
|
2130
|
+
try {
|
|
2131
|
+
const seedCore = new TaskforceCore({
|
|
2132
|
+
projectRoot,
|
|
2133
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2134
|
+
});
|
|
2135
|
+
const existing = seedCore.resolveAiProfile({
|
|
2136
|
+
workspaceId: 'workspace-1',
|
|
2137
|
+
name: 'Felix Leiter',
|
|
2138
|
+
provider: 'OpenAI',
|
|
2139
|
+
model: 'openai-codex/gpt-5.4',
|
|
2140
|
+
surfaceType: 'agent',
|
|
2141
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
2142
|
+
seedCore.close();
|
|
2143
|
+
const payload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
|
|
2144
|
+
workspaceId: 'workspace-1',
|
|
2145
|
+
resolveProfile: false,
|
|
2146
|
+
authAiProfileToken: 'tfp_stale_felix_leiter_token',
|
|
2147
|
+
authAiProfileName: 'Felix Leiter',
|
|
2148
|
+
authAiProfileProvider: 'OpenClaw',
|
|
2149
|
+
authAiProfileModel: 'gpt-5.4',
|
|
2150
|
+
authAiProfileSurfaceType: 'agent'
|
|
2151
|
+
}));
|
|
2152
|
+
expect(payload.identityBound).toBe(true);
|
|
2153
|
+
expect(payload.bindingSource).toBe('connection_bootstrap');
|
|
2154
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
2155
|
+
id: existing.profile.id,
|
|
2156
|
+
name: 'Felix Leiter',
|
|
2157
|
+
provider: 'OpenClaw',
|
|
2158
|
+
model: 'gpt-5.4',
|
|
2159
|
+
seatScope: 'cloud_metered',
|
|
2160
|
+
}));
|
|
2161
|
+
const verifyCore = new TaskforceCore({
|
|
2162
|
+
projectRoot,
|
|
2163
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2164
|
+
});
|
|
2165
|
+
expect(verifyCore.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
2166
|
+
verifyCore.close();
|
|
2167
|
+
}
|
|
2168
|
+
finally {
|
|
2169
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
1486
2172
|
it('searches tasks by T-XXX reference label via reference_number', async () => {
|
|
1487
2173
|
const projectRoot = createProjectRoot();
|
|
1488
2174
|
try {
|
|
@@ -1603,68 +2289,274 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1603
2289
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1604
2290
|
}
|
|
1605
2291
|
});
|
|
1606
|
-
it('manages planning entities, relationships, and structured checklist tools', async () => {
|
|
2292
|
+
it('manages planning entities, relationships, and structured checklist tools', async () => {
|
|
2293
|
+
const projectRoot = createProjectRoot();
|
|
2294
|
+
try {
|
|
2295
|
+
const createdInitiative = parseToolJson(await callTool(projectRoot, 'local', 'create_initiative', {
|
|
2296
|
+
title: 'Launch initiative'
|
|
2297
|
+
}));
|
|
2298
|
+
expect(createdInitiative.initiative.referenceLabel).toMatch(/^IN-/);
|
|
2299
|
+
const createdWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
2300
|
+
title: 'Launch workstream',
|
|
2301
|
+
initiativeId: createdInitiative.initiative.referenceLabel
|
|
2302
|
+
}));
|
|
2303
|
+
expect(createdWorkstream.workstream.referenceLabel).toMatch(/^WS-/);
|
|
2304
|
+
expect(createdWorkstream.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
2305
|
+
const createdTask = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
2306
|
+
title: 'Task for MCP planning'
|
|
2307
|
+
}));
|
|
2308
|
+
const taskLinked = parseToolJson(await callTool(projectRoot, 'local', 'set_task_workstream', {
|
|
2309
|
+
taskId: createdTask.referenceLabel,
|
|
2310
|
+
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
2311
|
+
}));
|
|
2312
|
+
expect(taskLinked.workstreamId).toBe(createdWorkstream.workstream.id);
|
|
2313
|
+
const checklistReplaced = parseToolJson(await callTool(projectRoot, 'local', 'replace_task_checklist', {
|
|
2314
|
+
taskId: createdTask.referenceLabel,
|
|
2315
|
+
items: [
|
|
2316
|
+
{ title: 'First checklist item', isCompleted: false },
|
|
2317
|
+
{ title: 'Second checklist item', isCompleted: true }
|
|
2318
|
+
]
|
|
2319
|
+
}));
|
|
2320
|
+
expect(checklistReplaced.items).toHaveLength(2);
|
|
2321
|
+
const checklistRead = parseToolJson(await callTool(projectRoot, 'local', 'get_task_checklist', {
|
|
2322
|
+
taskId: createdTask.referenceLabel
|
|
2323
|
+
}));
|
|
2324
|
+
expect(checklistRead.items.map((item) => item.title)).toEqual(['First checklist item', 'Second checklist item']);
|
|
2325
|
+
const workstreamCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_workstream_initiative', {
|
|
2326
|
+
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
2327
|
+
}));
|
|
2328
|
+
expect(workstreamCleared.workstream.initiativeId).toBeNull();
|
|
2329
|
+
const workstreamReset = parseToolJson(await callTool(projectRoot, 'local', 'set_workstream_initiative', {
|
|
2330
|
+
workstreamId: createdWorkstream.workstream.referenceLabel,
|
|
2331
|
+
initiativeId: createdInitiative.initiative.referenceLabel
|
|
2332
|
+
}));
|
|
2333
|
+
expect(workstreamReset.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
2334
|
+
const archivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'archive_workstream', {
|
|
2335
|
+
id: createdWorkstream.workstream.referenceLabel
|
|
2336
|
+
}));
|
|
2337
|
+
expect(archivedWorkstream.workstream.isArchived).toBe(true);
|
|
2338
|
+
const unarchivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_workstream', {
|
|
2339
|
+
id: createdWorkstream.workstream.referenceLabel
|
|
2340
|
+
}));
|
|
2341
|
+
expect(unarchivedWorkstream.workstream.isArchived).toBe(false);
|
|
2342
|
+
const archivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'archive_initiative', {
|
|
2343
|
+
id: createdInitiative.initiative.referenceLabel
|
|
2344
|
+
}));
|
|
2345
|
+
expect(archivedInitiative.initiative.isArchived).toBe(true);
|
|
2346
|
+
const unarchivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_initiative', {
|
|
2347
|
+
id: createdInitiative.initiative.referenceLabel
|
|
2348
|
+
}));
|
|
2349
|
+
expect(unarchivedInitiative.initiative.isArchived).toBe(false);
|
|
2350
|
+
const taskCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_task_workstream', {
|
|
2351
|
+
taskId: createdTask.referenceLabel
|
|
2352
|
+
}));
|
|
2353
|
+
expect(taskCleared.workstreamId).toBeNull();
|
|
2354
|
+
}
|
|
2355
|
+
finally {
|
|
2356
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
it('keeps the MCP runtime usable after linking a task to a workstream', async () => {
|
|
2360
|
+
const projectRoot = createProjectRoot();
|
|
2361
|
+
const runtime = createRuntime(projectRoot, 'local');
|
|
2362
|
+
try {
|
|
2363
|
+
const createdWorkstream = parseToolJson(await callToolWithRuntime(runtime, 'create_workstream', {
|
|
2364
|
+
title: 'Transport-safe workstream'
|
|
2365
|
+
}));
|
|
2366
|
+
const createdTask = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
|
|
2367
|
+
title: 'Transport-safe task'
|
|
2368
|
+
}));
|
|
2369
|
+
const taskLinked = parseToolJson(await callToolWithRuntime(runtime, 'set_task_workstream', {
|
|
2370
|
+
taskId: createdTask.referenceLabel,
|
|
2371
|
+
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
2372
|
+
}));
|
|
2373
|
+
expect(taskLinked.workstreamId).toBe(createdWorkstream.workstream.id);
|
|
2374
|
+
const workstreamRead = parseToolJson(await callToolWithRuntime(runtime, 'get_workstream', {
|
|
2375
|
+
id: createdWorkstream.workstream.referenceLabel
|
|
2376
|
+
}));
|
|
2377
|
+
expect(workstreamRead.workstream.id).toBe(createdWorkstream.workstream.id);
|
|
2378
|
+
expect(workstreamRead.tasks).toEqual(expect.arrayContaining([
|
|
2379
|
+
expect.objectContaining({
|
|
2380
|
+
id: createdTask.id,
|
|
2381
|
+
workstreamId: createdWorkstream.workstream.id
|
|
2382
|
+
})
|
|
2383
|
+
]));
|
|
2384
|
+
}
|
|
2385
|
+
finally {
|
|
2386
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
it('retries transient planning read failures before returning get_workstream results', async () => {
|
|
2390
|
+
const projectRoot = createProjectRoot();
|
|
2391
|
+
const runtime = createRuntime(projectRoot, 'cloud');
|
|
2392
|
+
try {
|
|
2393
|
+
const workstream = runtime.core.createWorkstream({ title: 'Retryable read workstream' }, 'workspace-1');
|
|
2394
|
+
const originalListWorkstreams = runtime.core.listWorkstreams.bind(runtime.core);
|
|
2395
|
+
let attempts = 0;
|
|
2396
|
+
const listWorkstreamsSpy = vi.spyOn(runtime.core, 'listWorkstreams').mockImplementation((workspaceId) => {
|
|
2397
|
+
attempts += 1;
|
|
2398
|
+
if (attempts === 1) {
|
|
2399
|
+
throw new Error('Postgres worker timed out while reading workstreams');
|
|
2400
|
+
}
|
|
2401
|
+
return originalListWorkstreams(workspaceId);
|
|
2402
|
+
});
|
|
2403
|
+
const workstreamRead = parseToolJson(await callToolWithRuntime(runtime, 'get_workstream', {
|
|
2404
|
+
id: workstream.referenceLabel
|
|
2405
|
+
}));
|
|
2406
|
+
expect(attempts).toBe(2);
|
|
2407
|
+
expect(workstreamRead.workstream.id).toBe(workstream.id);
|
|
2408
|
+
listWorkstreamsSpy.mockRestore();
|
|
2409
|
+
}
|
|
2410
|
+
finally {
|
|
2411
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
it('returns the created workstream when post-write workstream hydration times out', async () => {
|
|
2415
|
+
const projectRoot = createProjectRoot();
|
|
2416
|
+
const originalGetWorkstream = TaskforceCore.prototype.getWorkstream;
|
|
2417
|
+
let injected = false;
|
|
2418
|
+
const getWorkstreamSpy = vi.spyOn(TaskforceCore.prototype, 'getWorkstream').mockImplementation(function (id, workspaceId) {
|
|
2419
|
+
const result = originalGetWorkstream.call(this, id, workspaceId);
|
|
2420
|
+
if (!injected && result?.title === 'Hydration fallback workstream') {
|
|
2421
|
+
injected = true;
|
|
2422
|
+
throw new Error('Postgres worker timed out while reading workstreams');
|
|
2423
|
+
}
|
|
2424
|
+
return result;
|
|
2425
|
+
});
|
|
2426
|
+
try {
|
|
2427
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
2428
|
+
title: 'Hydration fallback workstream'
|
|
2429
|
+
}));
|
|
2430
|
+
expect(created.workstream.referenceLabel).toMatch(/^WS-/);
|
|
2431
|
+
expect(created.workstream.title).toBe('Hydration fallback workstream');
|
|
2432
|
+
}
|
|
2433
|
+
finally {
|
|
2434
|
+
getWorkstreamSpy.mockRestore();
|
|
2435
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
it('returns the created workstream when post-write response hydration times out', async () => {
|
|
2439
|
+
const projectRoot = createProjectRoot();
|
|
2440
|
+
const originalListTasks = TaskforceCore.prototype.listTasks;
|
|
2441
|
+
let injected = false;
|
|
2442
|
+
const listTasksSpy = vi.spyOn(TaskforceCore.prototype, 'listTasks').mockImplementation(function (workspaceId) {
|
|
2443
|
+
if (!injected) {
|
|
2444
|
+
injected = true;
|
|
2445
|
+
throw new Error('Postgres worker timed out while hydrating workstream response');
|
|
2446
|
+
}
|
|
2447
|
+
return originalListTasks.call(this, workspaceId);
|
|
2448
|
+
});
|
|
2449
|
+
try {
|
|
2450
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
2451
|
+
title: 'Response hydration fallback workstream'
|
|
2452
|
+
}));
|
|
2453
|
+
expect(created.workstream.referenceLabel).toMatch(/^WS-/);
|
|
2454
|
+
expect(created.workstream.title).toBe('Response hydration fallback workstream');
|
|
2455
|
+
expect(created.responseHydration).toEqual(expect.objectContaining({
|
|
2456
|
+
fallback: true,
|
|
2457
|
+
code: 'MCP_BACKEND_TIMEOUT',
|
|
2458
|
+
nextTool: 'get_workstream'
|
|
2459
|
+
}));
|
|
2460
|
+
}
|
|
2461
|
+
finally {
|
|
2462
|
+
listTasksSpy.mockRestore();
|
|
2463
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
it('retries transient get_initiative read failures before returning results', async () => {
|
|
2467
|
+
const projectRoot = createProjectRoot();
|
|
2468
|
+
const runtime = createRuntime(projectRoot, 'cloud');
|
|
2469
|
+
try {
|
|
2470
|
+
const initiative = runtime.core.createInitiative({ title: 'Retryable initiative read' }, 'workspace-1');
|
|
2471
|
+
const originalListInitiatives = runtime.core.listInitiatives.bind(runtime.core);
|
|
2472
|
+
let attempts = 0;
|
|
2473
|
+
const listInitiativesSpy = vi.spyOn(runtime.core, 'listInitiatives').mockImplementation((workspaceId) => {
|
|
2474
|
+
attempts += 1;
|
|
2475
|
+
if (attempts === 1) {
|
|
2476
|
+
throw new Error('Postgres worker timed out while reading initiatives');
|
|
2477
|
+
}
|
|
2478
|
+
return originalListInitiatives(workspaceId);
|
|
2479
|
+
});
|
|
2480
|
+
const initiativeRead = parseToolJson(await callToolWithRuntime(runtime, 'get_initiative', {
|
|
2481
|
+
id: initiative.referenceLabel
|
|
2482
|
+
}));
|
|
2483
|
+
expect(attempts).toBe(2);
|
|
2484
|
+
expect(initiativeRead.initiative.id).toBe(initiative.id);
|
|
2485
|
+
listInitiativesSpy.mockRestore();
|
|
2486
|
+
}
|
|
2487
|
+
finally {
|
|
2488
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2489
|
+
}
|
|
2490
|
+
});
|
|
2491
|
+
it('keeps save_initiative_attachment successful when post-write initiative hydration times out', async () => {
|
|
2492
|
+
const projectRoot = createProjectRoot();
|
|
2493
|
+
try {
|
|
2494
|
+
const createdInitiative = parseToolJson(await callTool(projectRoot, 'local', 'create_initiative', {
|
|
2495
|
+
title: 'Hydration fallback initiative'
|
|
2496
|
+
}));
|
|
2497
|
+
const originalGetInitiative = TaskforceCore.prototype.getInitiative;
|
|
2498
|
+
let injected = false;
|
|
2499
|
+
const getInitiativeSpy = vi.spyOn(TaskforceCore.prototype, 'getInitiative').mockImplementation(function (id, workspaceId) {
|
|
2500
|
+
const result = originalGetInitiative.call(this, id, workspaceId);
|
|
2501
|
+
if (!injected
|
|
2502
|
+
&& result
|
|
2503
|
+
&& result.id === createdInitiative.initiative.id
|
|
2504
|
+
&& Array.isArray(result.attachments)
|
|
2505
|
+
&& result.attachments.some((item) => item && typeof item === 'object' && item.displayName === 'hydration-note.md')) {
|
|
2506
|
+
injected = true;
|
|
2507
|
+
throw new Error('Postgres worker timed out while reading initiatives');
|
|
2508
|
+
}
|
|
2509
|
+
return result;
|
|
2510
|
+
});
|
|
2511
|
+
try {
|
|
2512
|
+
const saveResult = await callTool(projectRoot, 'local', 'save_initiative_attachment', {
|
|
2513
|
+
id: createdInitiative.initiative.referenceLabel,
|
|
2514
|
+
filename: 'hydration-note.md',
|
|
2515
|
+
content: '# Hydration fallback\n'
|
|
2516
|
+
});
|
|
2517
|
+
expect(String(saveResult.content?.[0]?.text || '')).toContain('Initiative attachment saved and attached');
|
|
2518
|
+
}
|
|
2519
|
+
finally {
|
|
2520
|
+
getInitiativeSpy.mockRestore();
|
|
2521
|
+
}
|
|
2522
|
+
const attachments = parseToolJson(await callTool(projectRoot, 'local', 'get_initiative_attachments', {
|
|
2523
|
+
id: createdInitiative.initiative.referenceLabel
|
|
2524
|
+
}));
|
|
2525
|
+
expect(attachments.attachments).toEqual(expect.arrayContaining([
|
|
2526
|
+
expect.objectContaining({
|
|
2527
|
+
displayName: 'hydration-note.md'
|
|
2528
|
+
})
|
|
2529
|
+
]));
|
|
2530
|
+
}
|
|
2531
|
+
finally {
|
|
2532
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
it('returns the archived workstream when the post-archive refresh times out', async () => {
|
|
1607
2536
|
const projectRoot = createProjectRoot();
|
|
1608
2537
|
try {
|
|
1609
|
-
const
|
|
1610
|
-
title: '
|
|
1611
|
-
}));
|
|
1612
|
-
expect(createdInitiative.initiative.referenceLabel).toMatch(/^IN-/);
|
|
1613
|
-
const createdWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
1614
|
-
title: 'Launch workstream',
|
|
1615
|
-
initiativeId: createdInitiative.initiative.referenceLabel
|
|
1616
|
-
}));
|
|
1617
|
-
expect(createdWorkstream.workstream.referenceLabel).toMatch(/^WS-/);
|
|
1618
|
-
expect(createdWorkstream.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
1619
|
-
const createdTask = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1620
|
-
title: 'Task for MCP planning'
|
|
1621
|
-
}));
|
|
1622
|
-
const taskLinked = parseToolJson(await callTool(projectRoot, 'local', 'set_task_workstream', {
|
|
1623
|
-
taskId: createdTask.referenceLabel,
|
|
1624
|
-
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
1625
|
-
}));
|
|
1626
|
-
expect(taskLinked.workstreamId).toBe(createdWorkstream.workstream.id);
|
|
1627
|
-
const checklistReplaced = parseToolJson(await callTool(projectRoot, 'local', 'replace_task_checklist', {
|
|
1628
|
-
taskId: createdTask.referenceLabel,
|
|
1629
|
-
items: [
|
|
1630
|
-
{ title: 'First checklist item', isCompleted: false },
|
|
1631
|
-
{ title: 'Second checklist item', isCompleted: true }
|
|
1632
|
-
]
|
|
1633
|
-
}));
|
|
1634
|
-
expect(checklistReplaced.items).toHaveLength(2);
|
|
1635
|
-
const checklistRead = parseToolJson(await callTool(projectRoot, 'local', 'get_task_checklist', {
|
|
1636
|
-
taskId: createdTask.referenceLabel
|
|
1637
|
-
}));
|
|
1638
|
-
expect(checklistRead.items.map((item) => item.title)).toEqual(['First checklist item', 'Second checklist item']);
|
|
1639
|
-
const workstreamCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_workstream_initiative', {
|
|
1640
|
-
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
1641
|
-
}));
|
|
1642
|
-
expect(workstreamCleared.workstream.initiativeId).toBeNull();
|
|
1643
|
-
const workstreamReset = parseToolJson(await callTool(projectRoot, 'local', 'set_workstream_initiative', {
|
|
1644
|
-
workstreamId: createdWorkstream.workstream.referenceLabel,
|
|
1645
|
-
initiativeId: createdInitiative.initiative.referenceLabel
|
|
1646
|
-
}));
|
|
1647
|
-
expect(workstreamReset.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
1648
|
-
const archivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'archive_workstream', {
|
|
1649
|
-
id: createdWorkstream.workstream.referenceLabel
|
|
1650
|
-
}));
|
|
1651
|
-
expect(archivedWorkstream.workstream.isArchived).toBe(true);
|
|
1652
|
-
const unarchivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_workstream', {
|
|
1653
|
-
id: createdWorkstream.workstream.referenceLabel
|
|
1654
|
-
}));
|
|
1655
|
-
expect(unarchivedWorkstream.workstream.isArchived).toBe(false);
|
|
1656
|
-
const archivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'archive_initiative', {
|
|
1657
|
-
id: createdInitiative.initiative.referenceLabel
|
|
1658
|
-
}));
|
|
1659
|
-
expect(archivedInitiative.initiative.isArchived).toBe(true);
|
|
1660
|
-
const unarchivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_initiative', {
|
|
1661
|
-
id: createdInitiative.initiative.referenceLabel
|
|
1662
|
-
}));
|
|
1663
|
-
expect(unarchivedInitiative.initiative.isArchived).toBe(false);
|
|
1664
|
-
const taskCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_task_workstream', {
|
|
1665
|
-
taskId: createdTask.referenceLabel
|
|
2538
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
2539
|
+
title: 'Archivable hydration fallback workstream'
|
|
1666
2540
|
}));
|
|
1667
|
-
|
|
2541
|
+
const originalGetWorkstream = TaskforceCore.prototype.getWorkstream;
|
|
2542
|
+
let injected = false;
|
|
2543
|
+
const getWorkstreamSpy = vi.spyOn(TaskforceCore.prototype, 'getWorkstream').mockImplementation(function (id, workspaceId) {
|
|
2544
|
+
const result = originalGetWorkstream.call(this, id, workspaceId);
|
|
2545
|
+
if (!injected && result && result.id === created.workstream.id && result.isArchived === true) {
|
|
2546
|
+
injected = true;
|
|
2547
|
+
throw new Error('Postgres worker timed out while reading workstreams');
|
|
2548
|
+
}
|
|
2549
|
+
return result;
|
|
2550
|
+
});
|
|
2551
|
+
try {
|
|
2552
|
+
const archived = parseToolJson(await callTool(projectRoot, 'local', 'archive_workstream', {
|
|
2553
|
+
id: created.workstream.referenceLabel
|
|
2554
|
+
}));
|
|
2555
|
+
expect(archived.workstream.isArchived).toBe(true);
|
|
2556
|
+
}
|
|
2557
|
+
finally {
|
|
2558
|
+
getWorkstreamSpy.mockRestore();
|
|
2559
|
+
}
|
|
1668
2560
|
}
|
|
1669
2561
|
finally {
|
|
1670
2562
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -1999,6 +2891,222 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
1999
2891
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2000
2892
|
}
|
|
2001
2893
|
});
|
|
2894
|
+
it('supports staged remote image uploads for save_task_attachment in cloud runtime', async () => {
|
|
2895
|
+
const projectRoot = createProjectRoot();
|
|
2896
|
+
const previousEnv = {
|
|
2897
|
+
provider: process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER,
|
|
2898
|
+
accountId: process.env.R2_ACCOUNT_ID,
|
|
2899
|
+
bucket: process.env.R2_BUCKET,
|
|
2900
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
2901
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
2902
|
+
endpoint: process.env.R2_ENDPOINT,
|
|
2903
|
+
uploadTtl: process.env.R2_SIGNED_UPLOAD_TTL_SECONDS,
|
|
2904
|
+
};
|
|
2905
|
+
try {
|
|
2906
|
+
process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER = 'r2';
|
|
2907
|
+
process.env.R2_ACCOUNT_ID = 'acct-1';
|
|
2908
|
+
process.env.R2_BUCKET = 'taskforce-test';
|
|
2909
|
+
process.env.R2_ACCESS_KEY_ID = 'access-key';
|
|
2910
|
+
process.env.R2_SECRET_ACCESS_KEY = 'secret-key';
|
|
2911
|
+
process.env.R2_ENDPOINT = 'https://example.r2.invalid';
|
|
2912
|
+
process.env.R2_SIGNED_UPLOAD_TTL_SECONDS = '600';
|
|
2913
|
+
const core = new TaskforceCore({
|
|
2914
|
+
projectRoot,
|
|
2915
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2916
|
+
});
|
|
2917
|
+
const task = core.createTask({ title: 'Cloud staged image attachment test' }, { workspaceId: 'workspace-1' });
|
|
2918
|
+
core.close();
|
|
2919
|
+
const fetchMock = vi.fn(async (input, init) => {
|
|
2920
|
+
const method = String(init?.method || 'GET').toUpperCase();
|
|
2921
|
+
if (method === 'GET') {
|
|
2922
|
+
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
|
|
2923
|
+
status: 200,
|
|
2924
|
+
headers: { 'content-type': 'image/png' }
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
return new Response('', { status: 200 });
|
|
2928
|
+
});
|
|
2929
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2930
|
+
const staged = parseToolJson(await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2931
|
+
id: task.referenceLabel,
|
|
2932
|
+
filename: 'proof.png',
|
|
2933
|
+
mimeType: 'image/png',
|
|
2934
|
+
sizeBytes: 4
|
|
2935
|
+
}));
|
|
2936
|
+
expect(staged.mode).toBe('staged_upload');
|
|
2937
|
+
expect(staged.status).toBe('upload_required');
|
|
2938
|
+
expect(staged.filename).toBe('proof.png');
|
|
2939
|
+
expect(staged.upload.method).toBe('PUT');
|
|
2940
|
+
expect(staged.upload.headers['Content-Type']).toBe('image/png');
|
|
2941
|
+
expect(staged.upload.uploadUrl).toContain('X-Amz-Signature=');
|
|
2942
|
+
expect(staged.upload.uploadedPath).toContain('workspaces/workspace-1/assets/images/');
|
|
2943
|
+
expect(staged.uploadDraftId).toContain('task-attachment-draft-');
|
|
2944
|
+
expect(staged.finalize.tool).toBe('save_task_attachment');
|
|
2945
|
+
expect(staged.finalize.arguments.uploadDraftId).toBe(staged.uploadDraftId);
|
|
2946
|
+
expect(staged.finalize.arguments.uploadedPath).toBe(staged.upload.uploadedPath);
|
|
2947
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2948
|
+
id: task.referenceLabel,
|
|
2949
|
+
filename: 'proof.png',
|
|
2950
|
+
mimeType: 'image/png',
|
|
2951
|
+
uploadDraftId: staged.uploadDraftId,
|
|
2952
|
+
uploadedPath: staged.upload.uploadedPath
|
|
2953
|
+
});
|
|
2954
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2955
|
+
expect(listed.attachments).toHaveLength(1);
|
|
2956
|
+
expect(listed.attachments[0]).toMatchObject({
|
|
2957
|
+
kind: 'image',
|
|
2958
|
+
mimeType: 'image/png',
|
|
2959
|
+
originalFilename: 'proof.png',
|
|
2960
|
+
readableAsText: false,
|
|
2961
|
+
});
|
|
2962
|
+
const verifyCore = new TaskforceCore({
|
|
2963
|
+
projectRoot,
|
|
2964
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2965
|
+
});
|
|
2966
|
+
try {
|
|
2967
|
+
const updated = verifyCore.getTask(task.id, 'workspace-1');
|
|
2968
|
+
const store = new WorkspaceAssetStore(verifyCore.getDatabaseAdapter(), verifyCore.getTenantId());
|
|
2969
|
+
const asset = store.get(String(listed.attachments[0]?.assetId || ''), 'workspace-1');
|
|
2970
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
2971
|
+
expect(updated?.attachments?.[0]).toMatchObject({
|
|
2972
|
+
path: expect.stringMatching(/^\/api\/taskforce\/context\//),
|
|
2973
|
+
originalFilename: 'proof.png',
|
|
2974
|
+
});
|
|
2975
|
+
expect(asset).toEqual(expect.objectContaining({
|
|
2976
|
+
kind: 'image',
|
|
2977
|
+
mimeType: 'image/png',
|
|
2978
|
+
storageProvider: 'r2',
|
|
2979
|
+
storageKey: staged.upload.uploadedPath,
|
|
2980
|
+
}));
|
|
2981
|
+
}
|
|
2982
|
+
finally {
|
|
2983
|
+
verifyCore.close();
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
finally {
|
|
2987
|
+
vi.unstubAllGlobals();
|
|
2988
|
+
if (previousEnv.provider === undefined)
|
|
2989
|
+
delete process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER;
|
|
2990
|
+
else
|
|
2991
|
+
process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER = previousEnv.provider;
|
|
2992
|
+
if (previousEnv.accountId === undefined)
|
|
2993
|
+
delete process.env.R2_ACCOUNT_ID;
|
|
2994
|
+
else
|
|
2995
|
+
process.env.R2_ACCOUNT_ID = previousEnv.accountId;
|
|
2996
|
+
if (previousEnv.bucket === undefined)
|
|
2997
|
+
delete process.env.R2_BUCKET;
|
|
2998
|
+
else
|
|
2999
|
+
process.env.R2_BUCKET = previousEnv.bucket;
|
|
3000
|
+
if (previousEnv.accessKeyId === undefined)
|
|
3001
|
+
delete process.env.R2_ACCESS_KEY_ID;
|
|
3002
|
+
else
|
|
3003
|
+
process.env.R2_ACCESS_KEY_ID = previousEnv.accessKeyId;
|
|
3004
|
+
if (previousEnv.secretAccessKey === undefined)
|
|
3005
|
+
delete process.env.R2_SECRET_ACCESS_KEY;
|
|
3006
|
+
else
|
|
3007
|
+
process.env.R2_SECRET_ACCESS_KEY = previousEnv.secretAccessKey;
|
|
3008
|
+
if (previousEnv.endpoint === undefined)
|
|
3009
|
+
delete process.env.R2_ENDPOINT;
|
|
3010
|
+
else
|
|
3011
|
+
process.env.R2_ENDPOINT = previousEnv.endpoint;
|
|
3012
|
+
if (previousEnv.uploadTtl === undefined)
|
|
3013
|
+
delete process.env.R2_SIGNED_UPLOAD_TTL_SECONDS;
|
|
3014
|
+
else
|
|
3015
|
+
process.env.R2_SIGNED_UPLOAD_TTL_SECONDS = previousEnv.uploadTtl;
|
|
3016
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
it('rejects forged staged upload finalization for save_task_attachment in cloud runtime', async () => {
|
|
3020
|
+
const projectRoot = createProjectRoot();
|
|
3021
|
+
const previousEnv = {
|
|
3022
|
+
provider: process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER,
|
|
3023
|
+
accountId: process.env.R2_ACCOUNT_ID,
|
|
3024
|
+
bucket: process.env.R2_BUCKET,
|
|
3025
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
3026
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
3027
|
+
endpoint: process.env.R2_ENDPOINT,
|
|
3028
|
+
uploadTtl: process.env.R2_SIGNED_UPLOAD_TTL_SECONDS,
|
|
3029
|
+
};
|
|
3030
|
+
try {
|
|
3031
|
+
process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER = 'r2';
|
|
3032
|
+
process.env.R2_ACCOUNT_ID = 'acct-1';
|
|
3033
|
+
process.env.R2_BUCKET = 'taskforce-test';
|
|
3034
|
+
process.env.R2_ACCESS_KEY_ID = 'access-key';
|
|
3035
|
+
process.env.R2_SECRET_ACCESS_KEY = 'secret-key';
|
|
3036
|
+
process.env.R2_ENDPOINT = 'https://example.r2.invalid';
|
|
3037
|
+
process.env.R2_SIGNED_UPLOAD_TTL_SECONDS = '600';
|
|
3038
|
+
const core = new TaskforceCore({
|
|
3039
|
+
projectRoot,
|
|
3040
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3041
|
+
});
|
|
3042
|
+
const task = core.createTask({ title: 'Cloud staged image attachment reject test' }, { workspaceId: 'workspace-1' });
|
|
3043
|
+
core.close();
|
|
3044
|
+
const fetchMock = vi.fn(async (input, init) => {
|
|
3045
|
+
const method = String(init?.method || 'GET').toUpperCase();
|
|
3046
|
+
if (method === 'GET') {
|
|
3047
|
+
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
|
|
3048
|
+
status: 200,
|
|
3049
|
+
headers: { 'content-type': 'image/png' }
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
return new Response('', { status: 200 });
|
|
3053
|
+
});
|
|
3054
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3055
|
+
const staged = parseToolJson(await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
3056
|
+
id: task.referenceLabel,
|
|
3057
|
+
filename: 'proof.png',
|
|
3058
|
+
mimeType: 'image/png',
|
|
3059
|
+
sizeBytes: 4
|
|
3060
|
+
}));
|
|
3061
|
+
const bareResult = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
3062
|
+
id: task.referenceLabel,
|
|
3063
|
+
filename: 'proof.png',
|
|
3064
|
+
mimeType: 'image/png',
|
|
3065
|
+
uploadedPath: staged.upload.uploadedPath
|
|
3066
|
+
});
|
|
3067
|
+
expect(bareResult.content?.[0]?.text).toContain('uploadedPath finalization requires uploadDraftId');
|
|
3068
|
+
const forgedResult = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
3069
|
+
id: task.referenceLabel,
|
|
3070
|
+
filename: 'proof.png',
|
|
3071
|
+
mimeType: 'image/png',
|
|
3072
|
+
uploadDraftId: staged.uploadDraftId,
|
|
3073
|
+
uploadedPath: 'workspaces/workspace-1/assets/images/asset-deadbeefdeadbeefdeadbeefdeadbeef-proof.png'
|
|
3074
|
+
});
|
|
3075
|
+
expect(forgedResult.content?.[0]?.text).toContain('uploadedPath does not match the staged upload draft');
|
|
3076
|
+
}
|
|
3077
|
+
finally {
|
|
3078
|
+
vi.unstubAllGlobals();
|
|
3079
|
+
if (previousEnv.provider === undefined)
|
|
3080
|
+
delete process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER;
|
|
3081
|
+
else
|
|
3082
|
+
process.env.TASKFORCE_OBJECT_STORAGE_PROVIDER = previousEnv.provider;
|
|
3083
|
+
if (previousEnv.accountId === undefined)
|
|
3084
|
+
delete process.env.R2_ACCOUNT_ID;
|
|
3085
|
+
else
|
|
3086
|
+
process.env.R2_ACCOUNT_ID = previousEnv.accountId;
|
|
3087
|
+
if (previousEnv.bucket === undefined)
|
|
3088
|
+
delete process.env.R2_BUCKET;
|
|
3089
|
+
else
|
|
3090
|
+
process.env.R2_BUCKET = previousEnv.bucket;
|
|
3091
|
+
if (previousEnv.accessKeyId === undefined)
|
|
3092
|
+
delete process.env.R2_ACCESS_KEY_ID;
|
|
3093
|
+
else
|
|
3094
|
+
process.env.R2_ACCESS_KEY_ID = previousEnv.accessKeyId;
|
|
3095
|
+
if (previousEnv.secretAccessKey === undefined)
|
|
3096
|
+
delete process.env.R2_SECRET_ACCESS_KEY;
|
|
3097
|
+
else
|
|
3098
|
+
process.env.R2_SECRET_ACCESS_KEY = previousEnv.secretAccessKey;
|
|
3099
|
+
if (previousEnv.endpoint === undefined)
|
|
3100
|
+
delete process.env.R2_ENDPOINT;
|
|
3101
|
+
else
|
|
3102
|
+
process.env.R2_ENDPOINT = previousEnv.endpoint;
|
|
3103
|
+
if (previousEnv.uploadTtl === undefined)
|
|
3104
|
+
delete process.env.R2_SIGNED_UPLOAD_TTL_SECONDS;
|
|
3105
|
+
else
|
|
3106
|
+
process.env.R2_SIGNED_UPLOAD_TTL_SECONDS = previousEnv.uploadTtl;
|
|
3107
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
2002
3110
|
it('allows generic markdown review attachments without requiring the legacy review template', async () => {
|
|
2003
3111
|
const projectRoot = createProjectRoot();
|
|
2004
3112
|
try {
|
|
@@ -2608,6 +3716,146 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2608
3716
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2609
3717
|
}
|
|
2610
3718
|
});
|
|
3719
|
+
it('saves, lists, and reads workstream attachments by workstream reference', async () => {
|
|
3720
|
+
const projectRoot = createProjectRoot();
|
|
3721
|
+
try {
|
|
3722
|
+
const core = new TaskforceCore({
|
|
3723
|
+
projectRoot,
|
|
3724
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3725
|
+
});
|
|
3726
|
+
const workstream = core.createWorkstream({ title: 'Attachment workstream' }, 'workspace-1');
|
|
3727
|
+
core.close();
|
|
3728
|
+
await callTool(projectRoot, 'cloud', 'save_workstream_attachment', {
|
|
3729
|
+
id: workstream.referenceLabel,
|
|
3730
|
+
filename: 'findings.md',
|
|
3731
|
+
content: '# Findings\nWorkstream-specific notes',
|
|
3732
|
+
caption: 'Security findings'
|
|
3733
|
+
});
|
|
3734
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_workstream_attachments', { id: workstream.referenceLabel }));
|
|
3735
|
+
const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_workstream_attachment', {
|
|
3736
|
+
workstreamId: workstream.referenceLabel,
|
|
3737
|
+
attachmentId: listed.attachments[0]?.attachmentId
|
|
3738
|
+
}));
|
|
3739
|
+
const workstreamPayload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_workstream', { id: workstream.referenceLabel }));
|
|
3740
|
+
const taskToolResult = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
3741
|
+
id: workstream.referenceLabel,
|
|
3742
|
+
filename: 'wrong-tool.md',
|
|
3743
|
+
content: '# Wrong tool'
|
|
3744
|
+
});
|
|
3745
|
+
const verifyCore = new TaskforceCore({
|
|
3746
|
+
projectRoot,
|
|
3747
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3748
|
+
});
|
|
3749
|
+
try {
|
|
3750
|
+
const store = new WorkspaceAssetStore(verifyCore.getDatabaseAdapter(), verifyCore.getTenantId());
|
|
3751
|
+
const links = store.listLinksForAsset(String(listed.attachments[0]?.assetId || ''), 'workspace-1');
|
|
3752
|
+
expect(listed.workstreamId).toBe(workstream.id);
|
|
3753
|
+
expect(listed.referenceLabel).toBe(workstream.referenceLabel);
|
|
3754
|
+
expect(listed.attachments).toHaveLength(1);
|
|
3755
|
+
expect(listed.attachments[0]?.kind).toBe('document');
|
|
3756
|
+
expect(listed.attachments[0]?.readableAsText).toBe(true);
|
|
3757
|
+
expect(readBack.workstreamId).toBe(workstream.id);
|
|
3758
|
+
expect(readBack.content).toContain('Workstream-specific notes');
|
|
3759
|
+
expect(workstreamPayload.workstream.attachments).toHaveLength(1);
|
|
3760
|
+
expect(workstreamPayload.workstream.attachments[0]).toMatchObject({
|
|
3761
|
+
assetId: listed.attachments[0]?.assetId,
|
|
3762
|
+
referenceLabel: listed.attachments[0]?.referenceLabel,
|
|
3763
|
+
originalFilename: 'findings.md',
|
|
3764
|
+
});
|
|
3765
|
+
expect(links).toEqual([
|
|
3766
|
+
expect.objectContaining({
|
|
3767
|
+
targetType: 'workstream',
|
|
3768
|
+
targetId: workstream.id,
|
|
3769
|
+
role: 'attachment'
|
|
3770
|
+
})
|
|
3771
|
+
]);
|
|
3772
|
+
expect(taskToolResult.content?.[0]?.text).toContain(`Task ${workstream.referenceLabel} not found`);
|
|
3773
|
+
}
|
|
3774
|
+
finally {
|
|
3775
|
+
verifyCore.close();
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
finally {
|
|
3779
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3782
|
+
it('saves and deletes initiative attachments by initiative reference', async () => {
|
|
3783
|
+
const projectRoot = createProjectRoot();
|
|
3784
|
+
try {
|
|
3785
|
+
const core = new TaskforceCore({
|
|
3786
|
+
projectRoot,
|
|
3787
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3788
|
+
});
|
|
3789
|
+
const initiative = core.createInitiative({ title: 'Attachment initiative' }, 'workspace-1');
|
|
3790
|
+
core.close();
|
|
3791
|
+
await callTool(projectRoot, 'cloud', 'save_initiative_attachment', {
|
|
3792
|
+
id: initiative.referenceLabel,
|
|
3793
|
+
filename: 'initiative-note.txt',
|
|
3794
|
+
content: 'initiative attachment body'
|
|
3795
|
+
});
|
|
3796
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_initiative_attachments', { id: initiative.referenceLabel }));
|
|
3797
|
+
const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_initiative_attachment', {
|
|
3798
|
+
initiativeId: initiative.referenceLabel,
|
|
3799
|
+
attachmentId: listed.attachments[0]?.attachmentId
|
|
3800
|
+
}));
|
|
3801
|
+
const deleted = parseToolJson(await callTool(projectRoot, 'cloud', 'delete_initiative_attachment', {
|
|
3802
|
+
initiativeId: initiative.referenceLabel,
|
|
3803
|
+
attachmentId: listed.attachments[0]?.attachmentId
|
|
3804
|
+
}));
|
|
3805
|
+
const afterDelete = parseToolJson(await callTool(projectRoot, 'cloud', 'get_initiative_attachments', { id: initiative.referenceLabel }));
|
|
3806
|
+
const verifyCore = new TaskforceCore({
|
|
3807
|
+
projectRoot,
|
|
3808
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3809
|
+
});
|
|
3810
|
+
try {
|
|
3811
|
+
const store = new WorkspaceAssetStore(verifyCore.getDatabaseAdapter(), verifyCore.getTenantId());
|
|
3812
|
+
const asset = store.get(String(listed.attachments[0]?.assetId || ''), 'workspace-1');
|
|
3813
|
+
expect(listed.initiativeId).toBe(initiative.id);
|
|
3814
|
+
expect(listed.attachments).toHaveLength(1);
|
|
3815
|
+
expect(listed.attachments[0]?.mimeType).toBe('text/plain');
|
|
3816
|
+
expect(readBack.content).toContain('initiative attachment body');
|
|
3817
|
+
expect(deleted.success).toBe(true);
|
|
3818
|
+
expect(deleted.canonicalAssetDeleted).toBe(true);
|
|
3819
|
+
expect(afterDelete.attachments).toEqual([]);
|
|
3820
|
+
expect(asset?.deletedAt).toBeTruthy();
|
|
3821
|
+
}
|
|
3822
|
+
finally {
|
|
3823
|
+
verifyCore.close();
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
finally {
|
|
3827
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
it('persists planning attachment descriptor payloads through update tools', async () => {
|
|
3831
|
+
const projectRoot = createProjectRoot();
|
|
3832
|
+
try {
|
|
3833
|
+
const core = new TaskforceCore({
|
|
3834
|
+
projectRoot,
|
|
3835
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3836
|
+
});
|
|
3837
|
+
const initiative = core.createInitiative({ title: 'Descriptor initiative' }, 'workspace-1');
|
|
3838
|
+
const workstream = core.createWorkstream({ title: 'Descriptor workstream' }, 'workspace-1');
|
|
3839
|
+
core.close();
|
|
3840
|
+
const initiativeAttachment = { path: 'https://example.com/initiative-register.md', caption: 'Initiative register' };
|
|
3841
|
+
const workstreamAttachment = { path: 'https://example.com/workstream-register.md', caption: 'Workstream register' };
|
|
3842
|
+
await callTool(projectRoot, 'cloud', 'update_initiative', {
|
|
3843
|
+
id: initiative.referenceLabel,
|
|
3844
|
+
attachments: [initiativeAttachment]
|
|
3845
|
+
});
|
|
3846
|
+
await callTool(projectRoot, 'cloud', 'update_workstream', {
|
|
3847
|
+
id: workstream.referenceLabel,
|
|
3848
|
+
attachments: [workstreamAttachment]
|
|
3849
|
+
});
|
|
3850
|
+
const initiativePayload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_initiative', { id: initiative.referenceLabel }));
|
|
3851
|
+
const workstreamPayload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_workstream', { id: workstream.referenceLabel }));
|
|
3852
|
+
expect(initiativePayload.initiative.attachments).toEqual([expect.objectContaining(initiativeAttachment)]);
|
|
3853
|
+
expect(workstreamPayload.workstream.attachments).toEqual([expect.objectContaining(workstreamAttachment)]);
|
|
3854
|
+
}
|
|
3855
|
+
finally {
|
|
3856
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3857
|
+
}
|
|
3858
|
+
});
|
|
2611
3859
|
it('accepts task references for write and batch update tools', async () => {
|
|
2612
3860
|
const projectRoot = createProjectRoot();
|
|
2613
3861
|
try {
|
|
@@ -2645,6 +3893,52 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2645
3893
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2646
3894
|
}
|
|
2647
3895
|
});
|
|
3896
|
+
it('accepts documented schedule helper arguments', async () => {
|
|
3897
|
+
const projectRoot = createProjectRoot();
|
|
3898
|
+
try {
|
|
3899
|
+
const core = new TaskforceCore({
|
|
3900
|
+
projectRoot,
|
|
3901
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3902
|
+
});
|
|
3903
|
+
const first = core.createTask({ title: 'First documented schedule helper test' }, { workspaceId: 'workspace-1' });
|
|
3904
|
+
const second = core.createTask({ title: 'Second documented schedule helper test' }, { workspaceId: 'workspace-1' });
|
|
3905
|
+
core.close();
|
|
3906
|
+
const board = parseToolJson(await callTool(projectRoot, 'local', 'get_schedule_board', {}));
|
|
3907
|
+
expect(board.week).toMatch(/^\d{4}-W\d{2}$/);
|
|
3908
|
+
const scheduled = parseToolJson(await callTool(projectRoot, 'local', 'schedule_task', {
|
|
3909
|
+
taskId: first.referenceLabel,
|
|
3910
|
+
date: '2099-01-05',
|
|
3911
|
+
order: 3
|
|
3912
|
+
}));
|
|
3913
|
+
expect(scheduled.scheduling.persistedDate).toBe('2099-01-05');
|
|
3914
|
+
expect(scheduled.scheduling.persistedOrderInDay).toBe(3);
|
|
3915
|
+
await callTool(projectRoot, 'local', 'reorder_scheduled_tasks', {
|
|
3916
|
+
date: '2099-01-05',
|
|
3917
|
+
taskIds: [second.referenceLabel, first.referenceLabel]
|
|
3918
|
+
});
|
|
3919
|
+
await callTool(projectRoot, 'local', 'unschedule_task', {
|
|
3920
|
+
taskId: first.referenceLabel
|
|
3921
|
+
});
|
|
3922
|
+
const after = new TaskforceCore({
|
|
3923
|
+
projectRoot,
|
|
3924
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3925
|
+
});
|
|
3926
|
+
try {
|
|
3927
|
+
const firstTask = after.getTask(first.id, 'workspace-1');
|
|
3928
|
+
const secondTask = after.getTask(second.id, 'workspace-1');
|
|
3929
|
+
expect(firstTask?.scheduledDate).toBeNull();
|
|
3930
|
+
expect(firstTask?.orderInDay).toBeNull();
|
|
3931
|
+
expect(secondTask?.scheduledDate).toBe('2099-01-05');
|
|
3932
|
+
expect(secondTask?.orderInDay).toBe(0);
|
|
3933
|
+
}
|
|
3934
|
+
finally {
|
|
3935
|
+
after.close();
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
finally {
|
|
3939
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
2648
3942
|
it('maps legacy agent assignee writes to unassigned even when a profile is resolved', async () => {
|
|
2649
3943
|
const projectRoot = createProjectRoot();
|
|
2650
3944
|
try {
|
|
@@ -2731,6 +4025,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2731
4025
|
label: 'Codex',
|
|
2732
4026
|
username: resolved.profile.username
|
|
2733
4027
|
}));
|
|
4028
|
+
expect(created.createdByActor.meta).not.toHaveProperty('profileToken');
|
|
2734
4029
|
expect(created.taskEvents).toEqual(expect.arrayContaining([
|
|
2735
4030
|
expect.objectContaining({
|
|
2736
4031
|
action: 'task-created',
|
|
@@ -2743,6 +4038,7 @@ describe('Taskforce MCP tool descriptions', () => {
|
|
|
2743
4038
|
})
|
|
2744
4039
|
})
|
|
2745
4040
|
]));
|
|
4041
|
+
expect(created.taskEvents[0].actorProfile.meta).not.toHaveProperty('profileToken');
|
|
2746
4042
|
}
|
|
2747
4043
|
finally {
|
|
2748
4044
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -3216,6 +4512,61 @@ describe('Taskforce MCP annotated attachment tools', () => {
|
|
|
3216
4512
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3217
4513
|
}
|
|
3218
4514
|
});
|
|
4515
|
+
it('accepts exposed document and image helper argument names', async () => {
|
|
4516
|
+
const projectRoot = createProjectRoot();
|
|
4517
|
+
try {
|
|
4518
|
+
const { task, session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
4519
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
4520
|
+
const taskAttachments = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', {
|
|
4521
|
+
taskId: task.referenceLabel,
|
|
4522
|
+
}, { workspaceId: 'workspace-1' }));
|
|
4523
|
+
expect(taskAttachments.taskId).toBe(task.id);
|
|
4524
|
+
expect(taskAttachments.referenceLabel).toBe(task.referenceLabel);
|
|
4525
|
+
const document = parseToolJson(await callTool(projectRoot, 'cloud', 'get_document', {
|
|
4526
|
+
id: 'D-123',
|
|
4527
|
+
}, { workspaceId: 'workspace-1' }));
|
|
4528
|
+
expect(document.document).toMatchObject({
|
|
4529
|
+
assetId: asset.assetId,
|
|
4530
|
+
referenceLabel: 'D-123',
|
|
4531
|
+
});
|
|
4532
|
+
const review = parseToolJson(await callTool(projectRoot, 'cloud', 'review_document', {
|
|
4533
|
+
documentId: 'D-123',
|
|
4534
|
+
prompt: 'Capture this as a first-pass review note.',
|
|
4535
|
+
}, { workspaceId: 'workspace-1' }));
|
|
4536
|
+
expect(review.document.assetId).toBe(asset.assetId);
|
|
4537
|
+
expect(review.comments).toEqual(expect.arrayContaining([
|
|
4538
|
+
expect.objectContaining({
|
|
4539
|
+
body: 'Capture this as a first-pass review note.',
|
|
4540
|
+
}),
|
|
4541
|
+
]));
|
|
4542
|
+
expect(review.session.comments.some((comment) => comment.body === 'Capture this as a first-pass review note.')).toBe(true);
|
|
4543
|
+
const image = parseToolJson(await callTool(projectRoot, 'cloud', 'get_image', {
|
|
4544
|
+
id: 'I-1',
|
|
4545
|
+
}, { workspaceId: 'workspace-1' }));
|
|
4546
|
+
expect(image.image).toMatchObject({
|
|
4547
|
+
assetId: 'asset-annotated-1',
|
|
4548
|
+
referenceLabel: 'I-1',
|
|
4549
|
+
});
|
|
4550
|
+
const marker = parseToolJson(await callTool(projectRoot, 'cloud', 'add_annotated_attachment_marker', {
|
|
4551
|
+
sessionId: session.id,
|
|
4552
|
+
type: 'note',
|
|
4553
|
+
text: 'Schema names should work end to end.',
|
|
4554
|
+
x: 0.4,
|
|
4555
|
+
y: 0.5,
|
|
4556
|
+
}, { workspaceId: 'workspace-1' }));
|
|
4557
|
+
expect(marker.marker).toMatchObject({
|
|
4558
|
+
kind: 'text-note',
|
|
4559
|
+
instruction: 'Schema names should work end to end.',
|
|
4560
|
+
});
|
|
4561
|
+
expect(marker.session.annotations.at(-1)).toMatchObject({
|
|
4562
|
+
kind: 'text-note',
|
|
4563
|
+
instruction: 'Schema names should work end to end.',
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
finally {
|
|
4567
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
3219
4570
|
it('creates image-note sessions and mutates markers through MCP', async () => {
|
|
3220
4571
|
const projectRoot = createProjectRoot();
|
|
3221
4572
|
try {
|