@taskforcehq/taskforce 0.3.170 → 0.3.300
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 +52 -21
- package/dist/Taskforce.module.css +3394 -5981
- package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
- package/dist/TaskforceCore.filter-persistence.test.js +402 -0
- package/dist/TaskforceCore.js +388 -71
- package/dist/TaskforceCore.routing.test.js +311 -0
- package/dist/TaskforceCore.test.js +3344 -615
- package/dist/TaskforceWidget.js +3 -2
- package/dist/__tests__/localizationCatalogs.test.js +48 -31
- package/dist/__tests__/workflowSimulation.test.js +2 -2
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
- package/dist/annotatedAttachments/payload.d.ts +2 -0
- package/dist/annotatedAttachments/payload.js +54 -0
- package/dist/annotatedAttachments/payload.test.js +67 -0
- package/dist/annotatedAttachments/service.d.ts +49 -0
- package/dist/annotatedAttachments/service.js +121 -0
- package/dist/annotatedAttachments/types.d.ts +86 -0
- package/dist/cli.js +74 -25
- package/dist/compat/deploymentCompat.d.ts +12 -0
- package/dist/compat/deploymentCompat.js +43 -0
- package/dist/compat/workspaceSyncCompat.d.ts +6 -0
- package/dist/compat/workspaceSyncCompat.js +79 -0
- package/dist/compat/workspaceSyncCompat.test.js +87 -0
- package/dist/components/features/AgentsModule.d.ts +5 -0
- package/dist/components/features/AgentsModule.js +131 -0
- package/dist/components/features/AgentsModule.test.js +102 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
- package/dist/components/features/DocumentGenerateModal.js +8 -16
- package/dist/components/features/DocumentIndex.d.ts +3 -4
- package/dist/components/features/DocumentIndex.js +22 -10
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +1000 -18
- package/dist/components/features/DocumentWorkspace.d.ts +40 -2
- package/dist/components/features/DocumentWorkspace.js +96 -16
- package/dist/components/features/DocumentWorkspace.test.js +1202 -0
- package/dist/components/features/InitiativesModule.d.ts +22 -0
- package/dist/components/features/InitiativesModule.js +33 -0
- package/dist/components/features/TaskSettings.d.ts +41 -32
- package/dist/components/features/TaskSettings.js +798 -313
- package/dist/components/features/TaskSettings.test.js +1221 -47
- package/dist/components/features/WorkflowsModule.d.ts +27 -0
- package/dist/components/features/WorkflowsModule.js +21 -0
- package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
- package/dist/components/features/documentWorkspaceFilters.js +11 -0
- package/dist/components/features/settings/CategoryManager.d.ts +2 -0
- package/dist/components/features/settings/CategoryManager.js +15 -8
- package/dist/components/features/settings/PriorityManager.d.ts +18 -0
- package/dist/components/features/settings/PriorityManager.js +51 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
- package/dist/components/features/settings/TaxonomyEditor.js +41 -18
- package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
- package/dist/components/features/settings/TaxonomyManager.js +59 -27
- package/dist/components/features/settings/TypeManager.d.ts +11 -0
- package/dist/components/features/settings/TypeManager.js +48 -11
- package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
- package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
- package/dist/components/task/TaskActionHeader.d.ts +12 -2
- package/dist/components/task/TaskActionHeader.js +64 -8
- package/dist/components/task/TaskActionHeader.test.js +73 -0
- package/dist/components/task/TaskCard.d.ts +14 -16
- package/dist/components/task/TaskCard.js +144 -174
- package/dist/components/task/TaskCard.test.js +416 -64
- package/dist/components/task/TaskContextUpload.d.ts +6 -5
- package/dist/components/task/TaskContextUpload.js +199 -64
- package/dist/components/task/TaskContextUpload.test.js +331 -14
- package/dist/components/task/TaskForm.d.ts +23 -29
- package/dist/components/task/TaskForm.js +348 -169
- package/dist/components/task/TaskForm.test.js +419 -23
- package/dist/components/task/TaskKanban.d.ts +20 -21
- package/dist/components/task/TaskKanban.js +176 -316
- package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
- package/dist/components/task/TaskList.d.ts +22 -11
- package/dist/components/task/TaskList.js +98 -12
- package/dist/components/task/TaskList.test.js +206 -7
- package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
- package/dist/components/task/TaskReferenceBadge.js +5 -0
- package/dist/components/task/TaskStatusActions.d.ts +12 -0
- package/dist/components/task/TaskStatusActions.js +18 -0
- package/dist/components/task/TaskStatusActions.test.js +45 -0
- package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
- package/dist/components/ui/ImageReferenceBadge.js +5 -0
- package/dist/components/ui/LevelSelector.d.ts +2 -1
- package/dist/components/ui/LevelSelector.js +2 -2
- package/dist/components/ui/Markdown.d.ts +1 -2
- package/dist/components/ui/Markdown.js +2 -13
- package/dist/components/ui/Modal.d.ts +3 -2
- package/dist/components/ui/Modal.js +4 -3
- package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
- package/dist/components/ui/MultiSelectFilter.js +2 -2
- package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
- package/dist/components/ui/ReferenceBadgeButton.js +6 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
- package/dist/components/ui/TaxonomyDropdown.js +58 -23
- package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
- package/dist/components/ui/TopNoticeLayer.js +14 -0
- package/dist/components/ui/TopNoticeLayer.test.js +25 -0
- package/dist/components/views/AppShellHeader.d.ts +13 -0
- package/dist/components/views/AppShellHeader.js +19 -0
- package/dist/components/views/AppShellHeader.test.js +21 -0
- package/dist/components/views/PlanComparisonPage.d.ts +73 -0
- package/dist/components/views/PlanComparisonPage.js +196 -0
- package/dist/components/views/PlanComparisonPage.test.js +251 -0
- package/dist/components/views/PlansPage.d.ts +14 -0
- package/dist/components/views/PlansPage.js +355 -0
- package/dist/components/views/StandaloneLayout.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +2212 -1194
- package/dist/components/views/WidgetView.d.ts +2 -2
- package/dist/components/views/WidgetView.js +149 -43
- package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
- package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
- package/dist/components/views/chrome/workspaceChrome.js +108 -0
- package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
- package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
- package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
- package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
- package/dist/components/views/panels/FilterToolbar.d.ts +66 -0
- package/dist/components/views/panels/FilterToolbar.js +21 -0
- package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbar.test.js +45 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +46 -0
- package/dist/components/views/panels/FilterToolbarControls.js +61 -0
- package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
- package/dist/components/views/panels/PlanningDrawer.js +220 -0
- package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
- package/dist/components/views/panels/ScheduleSidebar.js +127 -0
- package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
- package/dist/components/views/panels/scheduleUtils.js +79 -0
- package/dist/components/views/teamManagementAccess.d.ts +6 -0
- package/dist/components/views/teamManagementAccess.js +18 -0
- package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
- package/dist/components/views/teamManagementAccess.test.js +44 -0
- package/dist/config/clientDeployment.d.ts +2 -0
- package/dist/config/clientDeployment.js +13 -0
- package/dist/config/cloudEnvironment.d.ts +15 -0
- package/dist/config/cloudEnvironment.js +60 -0
- package/dist/config/deployment.d.ts +20 -0
- package/dist/config/deployment.js +67 -0
- package/dist/config/deployment.test.d.ts +1 -0
- package/dist/config/deployment.test.js +88 -0
- package/dist/config/envSchema.js +36 -8
- package/dist/config/envSchema.test.js +28 -0
- package/dist/core/AiProfileService.d.ts +66 -0
- package/dist/core/AiProfileService.js +320 -0
- package/dist/core/AiProfileService.test.d.ts +1 -0
- package/dist/core/AiProfileService.test.js +164 -0
- package/dist/core/AiProfiles.test.d.ts +1 -0
- package/dist/core/AiProfiles.test.js +449 -0
- package/dist/core/AttachmentLinkService.d.ts +15 -0
- package/dist/core/AttachmentLinkService.js +157 -0
- package/dist/core/AuthTokenService.d.ts +44 -0
- package/dist/core/AuthTokenService.js +207 -0
- package/dist/core/CreatedBy.test.js +134 -6
- package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
- package/dist/core/DeletedTaskLifecycleService.js +158 -0
- package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
- package/dist/core/DeletedTaskTrash.test.js +104 -0
- package/dist/core/EntitlementsPolicy.test.js +78 -5
- package/dist/core/GlobalSettingsService.d.ts +16 -0
- package/dist/core/GlobalSettingsService.js +370 -0
- package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
- package/dist/core/JsonBackupRuntime.test.js +155 -0
- package/dist/core/McpTokenService.d.ts +118 -0
- package/dist/core/McpTokenService.js +921 -0
- package/dist/core/PlanEntitlementService.d.ts +201 -0
- package/dist/core/PlanEntitlementService.js +1442 -0
- package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
- package/dist/core/PlanVersionPolicy.test.js +178 -0
- package/dist/core/PlanningEntities.test.d.ts +1 -0
- package/dist/core/PlanningEntities.test.js +95 -0
- package/dist/core/ScheduleFields.test.js +15 -19
- package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
- package/dist/core/SignupMonetizationSettings.test.js +159 -0
- package/dist/core/Stats.test.d.ts +1 -0
- package/dist/core/Stats.test.js +36 -0
- package/dist/core/SyncReconciliationService.d.ts +93 -0
- package/dist/core/SyncReconciliationService.js +406 -0
- package/dist/core/SystemAdmin.test.js +578 -13
- package/dist/core/TaskActivityService.d.ts +21 -0
- package/dist/core/TaskActivityService.js +130 -0
- package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
- package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
- package/dist/core/TaskAuditTimeline.test.js +132 -0
- package/dist/core/TaskTaxonomyValidation.test.js +338 -15
- package/dist/core/Taskforce.d.ts +930 -183
- package/dist/core/Taskforce.ids.test.js +252 -0
- package/dist/core/Taskforce.js +3848 -2532
- package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
- package/dist/core/Taskforce.mcpAuth.test.js +343 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
- package/dist/core/TaxonomySettingsService.d.ts +18 -0
- package/dist/core/TaxonomySettingsService.js +162 -0
- package/dist/core/WorkspaceDelete.test.js +32 -13
- package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
- package/dist/core/WorkspaceLifecycleService.js +88 -0
- package/dist/core/WorkspacePermissions.test.js +95 -0
- package/dist/core/WorkspacePlanMode.test.js +114 -1
- package/dist/core/WorkspacePreferenceService.d.ts +41 -0
- package/dist/core/WorkspacePreferenceService.js +246 -0
- package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
- package/dist/core/WorkspaceSyncSettings.test.js +65 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
- package/dist/core/shared.d.ts +36 -0
- package/dist/core/shared.js +128 -0
- package/dist/core/types.d.ts +614 -0
- package/dist/core/types.js +27 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
- package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
- package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
- package/dist/documentReviews/service.d.ts +68 -0
- package/dist/documentReviews/service.js +210 -0
- package/dist/documentReviews/types.d.ts +58 -0
- package/dist/documentReviews/types.js +1 -0
- package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
- package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
- package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskActions.js +2 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
- package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFilters.js +2 -0
- package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
- package/dist/hooks/tasks/useTaskFormActions.js +365 -0
- package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
- package/dist/hooks/tasks/useTaskRelationships.js +64 -0
- package/dist/hooks/ui/useResourceExport.d.ts +19 -0
- package/dist/hooks/ui/useResourceExport.js +84 -0
- package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
- package/dist/hooks/ui/useSettingsModel.js +125 -0
- package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
- package/dist/hooks/ui/useSettingsPersistence.js +254 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
- package/dist/hooks/ui/useUiNotice.d.ts +1 -0
- package/dist/hooks/ui/useUiNotice.js +1 -0
- package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
- package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
- package/dist/hooks/ui/useZenMode.d.ts +5 -0
- package/dist/hooks/ui/useZenMode.js +32 -0
- package/dist/hooks/useFilterSort.d.ts +60 -0
- package/dist/hooks/useFilterSort.js +362 -0
- package/dist/hooks/useFilterSort.test.d.ts +1 -0
- package/dist/hooks/useFilterSort.test.js +333 -0
- package/dist/hooks/useRealtimeSync.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.js +24 -9
- package/dist/hooks/useRealtimeSync.test.js +28 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
- package/dist/hooks/useSyncOrchestrator.js +1538 -670
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
- package/dist/hooks/useTaskData.d.ts +35 -0
- package/dist/hooks/useTaskData.js +239 -0
- package/dist/hooks/useTaskData.test.d.ts +1 -0
- package/dist/hooks/useTaskData.test.js +119 -0
- package/dist/hooks/useTaskMutations.d.ts +46 -0
- package/dist/hooks/useTaskMutations.js +472 -0
- package/dist/hooks/useTaskMutations.test.d.ts +1 -0
- package/dist/hooks/useTaskMutations.test.js +80 -0
- package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.contract.test.js +7 -0
- package/dist/hooks/useTaskforce.d.ts +260 -295
- package/dist/hooks/useTaskforce.js +1954 -2623
- package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
- package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
- package/dist/hooks/useUiNotice.d.ts +16 -0
- package/dist/hooks/useUiNotice.js +49 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
- package/dist/hooks/useWorkspaceSyncController.js +280 -461
- package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
- package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
- package/dist/hooks/workspace/workspaceLocalState.js +101 -0
- package/dist/localization/locales/en-US.d.ts +1 -8
- package/dist/localization/locales/en-US.js +4 -11
- package/dist/localization/locales/es-419.js +2 -9
- package/dist/localization/locales/pt-BR.js +2 -9
- package/dist/localization/localizationCatalogs.test.d.ts +1 -0
- package/dist/localization/localizationCatalogs.test.js +37 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -15
- package/dist/mcp/clientIntegrations.d.ts +33 -0
- package/dist/mcp/clientIntegrations.js +199 -0
- package/dist/mcp/clientIntegrations.test.d.ts +1 -0
- package/dist/mcp/clientIntegrations.test.js +110 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/optionResolution.test.js +6 -2
- package/dist/mcp/runtime.d.ts +120 -0
- package/dist/mcp/runtime.js +5668 -0
- package/dist/mcp/runtime.test.d.ts +1 -0
- package/dist/mcp/runtime.test.js +3293 -0
- package/dist/mcp/server.js +2 -1793
- package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
- package/dist/mcp/structuredDocValidation.js +1 -2
- package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
- package/dist/mcp/toolRegistry.d.ts +570 -0
- package/dist/mcp/toolRegistry.js +90 -0
- package/dist/mcp/toolRegistry.test.d.ts +1 -0
- package/dist/mcp/toolRegistry.test.js +30 -0
- package/dist/migrations/billingSchemaParity.test.js +111 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
- package/dist/migrations/taskSchemaMigrations.js +681 -39
- package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.test.js +210 -0
- package/dist/plugins/vite.js +13 -7
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/favicon.ico +0 -0
- package/dist/public/archive/phoenix/site.webmanifest +1 -0
- package/dist/public/favicon-96x96.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.svg +17 -0
- package/dist/public/site.webmanifest +21 -1
- package/dist/public/web-app-manifest-192x192.png +0 -0
- package/dist/public/web-app-manifest-512x512.png +0 -0
- package/dist/releaseVersion.test.d.ts +1 -0
- package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/dist/resources/templates/workflows/collaborate.yaml +11 -5
- package/dist/resources/templates/workflows/do.yaml +8 -2
- package/dist/resources/templates/workflows/evaluate.yaml +13 -7
- package/dist/resources/templates/workflows/execute.yaml +40 -23
- package/dist/resources/templates/workflows/plan.yaml +15 -8
- package/dist/resources/templates/workflows/review.yaml +14 -8
- package/dist/runtime/appGateDefinitions.d.ts +32 -0
- package/dist/runtime/appGateDefinitions.js +32 -0
- package/dist/runtime/appGates.d.ts +29 -0
- package/dist/runtime/appGates.js +53 -0
- package/dist/runtime/appGates.test.d.ts +1 -0
- package/dist/runtime/appGates.test.js +42 -0
- package/dist/runtime/migrationFlags.d.ts +1 -0
- package/dist/runtime/migrationFlags.js +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
- package/dist/server/auth.js +14 -2
- package/dist/server/auth.test.js +21 -1
- package/dist/server/cors.js +11 -5
- package/dist/server/cors.test.js +6 -0
- package/dist/server/documentReviewRoutes.test.d.ts +1 -0
- package/dist/server/documentReviewRoutes.test.js +157 -0
- package/dist/server/index.cookieProxy.test.js +46 -1
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +168 -41
- package/dist/server/index.rateLimit.test.js +6 -0
- package/dist/server/index.test.js +133 -2
- package/dist/server/mockStripe.d.ts +72 -0
- package/dist/server/mockStripe.js +357 -0
- package/dist/server/realtimeSyncWs.js +4 -0
- package/dist/server/routes/admin.d.ts +114 -0
- package/dist/server/routes/admin.js +2226 -0
- package/dist/server/routes/annotatedAttachments.d.ts +24 -0
- package/dist/server/routes/annotatedAttachments.js +235 -0
- package/dist/server/routes/auth.d.ts +61 -0
- package/dist/server/routes/auth.js +1296 -0
- package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
- package/dist/server/routes/billing.js +2001 -0
- package/dist/server/routes/billing.test.d.ts +1 -0
- package/dist/server/routes/billing.test.js +1631 -0
- package/dist/server/routes/documentReviews.d.ts +11 -0
- package/dist/server/routes/documentReviews.js +191 -0
- package/dist/server/routes/documents.d.ts +118 -0
- package/dist/server/routes/documents.js +1113 -0
- package/dist/server/routes/resources.d.ts +25 -0
- package/dist/server/routes/resources.js +225 -0
- package/dist/server/routes/shared.d.ts +35 -0
- package/dist/server/routes/shared.js +305 -0
- package/dist/server/routes/shared.test.d.ts +1 -0
- package/dist/server/routes/shared.test.js +32 -0
- package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
- package/dist/server/routes/sync.integration.test.d.ts +1 -0
- package/dist/server/routes/sync.integration.test.js +2003 -0
- package/dist/server/routes/sync.js +3143 -0
- package/dist/server/routes/tasks.d.ts +72 -0
- package/dist/server/routes/tasks.js +1252 -0
- package/dist/server/routes/workspaces.d.ts +32 -0
- package/dist/server/routes/workspaces.js +307 -0
- package/dist/server/routes.d.ts +8 -9
- package/dist/server/routes.js +968 -4092
- package/dist/server/routes.test.js +9043 -2415
- package/dist/server/runtimeMode.d.ts +1 -1
- package/dist/server/runtimeMode.js +3 -18
- package/dist/server/runtimeMode.test.js +3 -3
- package/dist/server/securityHeaders.d.ts +1 -1
- package/dist/server/securityHeaders.js +65 -6
- package/dist/server/securityHeaders.test.js +40 -2
- package/dist/services/templateEngine.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
- package/dist/services/templates.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templates.test.js +1 -1
- package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
- package/dist/services/workflowExportMatrix.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
- package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/services/workflowGeneration.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.js +113 -0
- package/dist/services/workflowV2Policy.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
- package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
- package/dist/shared/aiProfileSurfaceType.js +49 -0
- package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
- package/dist/shared/customTaxonomyLibrary.js +276 -0
- package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/customTaxonomyLibrary.test.js +20 -0
- package/dist/shared/taxonomyCatalog.d.ts +19 -0
- package/dist/shared/taxonomyCatalog.js +36 -0
- package/dist/shared/taxonomyDefaults.d.ts +29 -0
- package/dist/shared/taxonomyDefaults.js +26 -0
- package/dist/shared/taxonomyLibrary.d.ts +15 -0
- package/dist/shared/taxonomyLibrary.js +98 -0
- package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/taxonomyLibrary.test.js +26 -0
- package/dist/shared/taxonomyState.d.ts +36 -0
- package/dist/shared/taxonomyState.js +73 -0
- package/dist/shared/taxonomyState.test.d.ts +1 -0
- package/dist/shared/taxonomyState.test.js +71 -0
- package/dist/shared/workflowDefaults.d.ts +9 -0
- package/dist/shared/workflowDefaults.js +11 -0
- package/dist/storage/documentIntegrity.d.ts +2 -0
- package/dist/storage/documentIntegrity.js +34 -17
- package/dist/storage/documentPurge.d.ts +2 -1
- package/dist/storage/documentPurge.js +35 -8
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/objectStorage.d.ts +1 -1
- package/dist/storage/objectStorageClient.d.ts +6 -4
- package/dist/storage/objectStorageClient.js +77 -29
- package/dist/storage/objectStorageClient.test.d.ts +1 -0
- package/dist/storage/objectStorageClient.test.js +57 -0
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.d.ts +1 -0
- package/dist/storage/objectStorageConfig.test.js +38 -0
- package/dist/storage/postgresAdapter.d.ts +5 -0
- package/dist/storage/postgresAdapter.js +2 -2
- package/dist/storage/postgresAdapter.test.d.ts +1 -0
- package/dist/storage/postgresAdapter.test.js +29 -0
- package/dist/storage/taskAssetStore.d.ts +7 -4
- package/dist/storage/taskAssetStore.js +65 -60
- package/dist/storage/workspaceAssetStore.d.ts +122 -0
- package/dist/storage/workspaceAssetStore.js +496 -0
- package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
- package/dist/storage/workspaceAssetStore.test.js +501 -0
- package/dist/sync/cloudSyncApi.d.ts +33 -2
- package/dist/sync/cloudSyncApi.js +37 -15
- package/dist/sync/cloudSyncApi.test.js +60 -3
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/syncService.d.ts +25 -0
- package/dist/sync/syncService.js +184 -16
- package/dist/sync/syncService.test.js +289 -25
- package/dist/sync/workspaceRepair.d.ts +114 -0
- package/dist/sync/workspaceRepair.js +566 -0
- package/dist/sync/workspaceSyncModel.d.ts +230 -2
- package/dist/sync/workspaceSyncModel.js +456 -17
- package/dist/sync/workspaceSyncModel.test.js +642 -0
- package/dist/sync/workspaceSyncState.d.ts +12 -0
- package/dist/sync/workspaceSyncState.js +12 -0
- package/dist/sync/workspaceSyncSurface.d.ts +33 -0
- package/dist/sync/workspaceSyncSurface.js +74 -0
- package/dist/types.d.ts +143 -18
- package/dist/types.js +9 -24
- package/dist/ui/assets/AgentsModule-TVmCgizS.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CAyhnPeR.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BOCw5e4D.js +250 -0
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
- package/dist/ui/assets/InitiativesModule-CN0r02Oi.js +1 -0
- package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
- package/dist/ui/assets/PlansPage-FcQSNLZd.js +1 -0
- package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +8 -0
- package/dist/ui/assets/WorkflowsModule-CY9R2zPB.js +5 -0
- package/dist/ui/assets/index-Bzqy5QGm.js +6 -0
- package/dist/ui/assets/index-DxRiI3tY.css +1 -0
- package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
- package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
- package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
- package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
- package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
- package/dist/ui/branding/logos/logo.png +0 -0
- package/dist/ui/branding/logos/logo_dark.svg +27 -0
- package/dist/ui/branding/logos/logo_favicon.svg +35 -0
- package/dist/ui/branding/logos/logo_light.svg +27 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
- package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
- package/dist/ui/favicon/favicon-96x96.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.svg +17 -0
- package/dist/ui/favicon/site.webmanifest +21 -1
- package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/ui/index.html +10 -7
- package/dist/ui/product/screen_01.png +0 -0
- package/dist/ui/product/screen_02.png +0 -0
- package/dist/ui/product/screen_03.png +0 -0
- package/dist/utils/annotatedAttachments.d.ts +20 -0
- package/dist/utils/annotatedAttachments.js +74 -0
- package/dist/utils/annotatedAttachments.test.d.ts +1 -0
- package/dist/utils/annotatedAttachments.test.js +108 -0
- package/dist/utils/assignees.d.ts +26 -0
- package/dist/utils/assignees.js +132 -0
- package/dist/utils/canonicalAssetKind.d.ts +16 -0
- package/dist/utils/canonicalAssetKind.js +39 -0
- package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
- package/dist/utils/canonicalAssetKind.test.js +24 -0
- package/dist/utils/coalescedRefresh.d.ts +5 -0
- package/dist/utils/coalescedRefresh.js +16 -0
- package/dist/utils/coalescedRefresh.test.d.ts +1 -0
- package/dist/utils/coalescedRefresh.test.js +30 -0
- package/dist/utils/constants.d.ts +16 -3
- package/dist/utils/constants.icons.test.js +2 -1
- package/dist/utils/constants.js +70 -13
- package/dist/utils/contextAssetEvents.d.ts +7 -0
- package/dist/utils/contextAssetEvents.js +6 -0
- package/dist/utils/contextFiles.d.ts +14 -3
- package/dist/utils/contextFiles.js +40 -8
- package/dist/utils/documentManagerApi.d.ts +6 -0
- package/dist/utils/documentManagerApi.js +19 -0
- package/dist/utils/documentNames.d.ts +24 -0
- package/dist/utils/documentNames.js +91 -0
- package/dist/utils/documentReferences.d.ts +9 -0
- package/dist/utils/documentReferences.js +11 -0
- package/dist/utils/entityReferences.d.ts +9 -0
- package/dist/utils/entityReferences.js +32 -0
- package/dist/utils/imageReferences.d.ts +9 -0
- package/dist/utils/imageReferences.js +11 -0
- package/dist/utils/planParser.d.ts +2 -2
- package/dist/utils/planParser.js +11 -11
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/{__tests__ → utils}/planParser.test.js +22 -22
- package/dist/utils/planningReferences.d.ts +17 -0
- package/dist/utils/planningReferences.js +30 -0
- package/dist/utils/planningReferences.test.d.ts +1 -0
- package/dist/utils/planningReferences.test.js +17 -0
- package/dist/utils/taskActivity.d.ts +9 -0
- package/dist/utils/taskActivity.js +147 -0
- package/dist/utils/taskActivity.test.d.ts +1 -0
- package/dist/utils/taskActivity.test.js +34 -0
- package/dist/utils/taskDraftDefaults.d.ts +21 -0
- package/dist/utils/taskDraftDefaults.js +40 -0
- package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
- package/dist/utils/taskDraftDefaults.test.js +34 -0
- package/dist/utils/taskNormalization.d.ts +25 -0
- package/dist/utils/taskNormalization.js +161 -0
- package/dist/utils/taskNormalization.test.d.ts +1 -0
- package/dist/utils/taskNormalization.test.js +158 -0
- package/dist/utils/taskReferences.d.ts +5 -0
- package/dist/utils/taskReferences.js +11 -0
- package/dist/utils/taskforceApiClient.d.ts +2 -0
- package/dist/utils/taskforceApiClient.js +101 -3
- package/dist/utils/taskforceApiClient.test.d.ts +1 -0
- package/dist/utils/taskforceApiClient.test.js +44 -0
- package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
- package/dist/utils/taxonomyFilterOptions.js +17 -0
- package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
- package/dist/utils/taxonomyFormDefaults.js +28 -0
- package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
- package/dist/utils/taxonomyFormDefaults.test.js +63 -0
- package/dist/utils/taxonomyLifecycle.d.ts +3 -0
- package/dist/utils/taxonomyLifecycle.js +17 -0
- package/dist/utils/taxonomySorting.d.ts +7 -0
- package/dist/utils/taxonomySorting.js +74 -0
- package/dist/utils/theme.d.ts +15 -0
- package/dist/utils/theme.js +23 -0
- package/dist/utils/uiNotice.d.ts +7 -0
- package/dist/utils/uiNotice.js +6 -0
- package/dist/utils/uiNotice.test.d.ts +1 -0
- package/dist/utils/uiNotice.test.js +18 -0
- package/dist/utils/workspaceIdentity.d.ts +14 -0
- package/dist/utils/workspaceIdentity.js +21 -0
- package/dist/utils/workspaceIdentity.test.js +30 -1
- package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
- package/dist/utils/workspaceSyncPresentation.js +21 -0
- package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
- package/dist/utils/workspaceSyncPresentation.test.js +36 -0
- package/package.json +28 -23
- package/scripts/bootstrap-playwright-auth.mjs +155 -0
- package/scripts/check-compat-boundaries.mjs +41 -0
- package/scripts/check-env.ts +9 -2
- package/scripts/debug-login.mjs +32 -0
- package/scripts/migrate-planning-model.ts +233 -0
- package/scripts/migrate-task-data-assets.mjs +408 -0
- package/scripts/playwright-auth.sh +11 -0
- package/scripts/run-dev-proxy.sh +2 -0
- package/scripts/update-homebrew-formula.sh +2 -2
- package/scripts/with-esbuild.sh +11 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/src/resources/templates/workflows/collaborate.yaml +11 -5
- package/src/resources/templates/workflows/do.yaml +8 -2
- package/src/resources/templates/workflows/evaluate.yaml +13 -7
- package/src/resources/templates/workflows/execute.yaml +40 -23
- package/src/resources/templates/workflows/plan.yaml +15 -8
- package/src/resources/templates/workflows/review.yaml +14 -8
- package/dist/TaskforceScreenshot.d.ts +0 -11
- package/dist/TaskforceScreenshot.js +0 -113
- package/dist/compat/priorityCompat.d.ts +0 -2
- package/dist/compat/priorityCompat.js +0 -24
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
- package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
- package/dist/components/task/TaskKanban 2.d.ts +0 -76
- package/dist/components/task/TaskKanban 2.js +0 -1004
- package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
- package/dist/components/task/TaskScreenshotSection.js +0 -22
- package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
- package/dist/components/views/StandaloneLayout 2.js +0 -1660
- package/dist/core/InitiativeTemplates.test.js +0 -50
- package/dist/core/ParentLifecycle.test.js +0 -98
- package/dist/migrations/clientMigrations.d.ts +0 -9
- package/dist/migrations/clientMigrations.js +0 -74
- package/dist/migrations/flags.d.ts +0 -6
- package/dist/migrations/flags.js +0 -8
- package/dist/migrations/taskMigrations.d.ts +0 -71
- package/dist/migrations/taskMigrations.js +0 -236
- package/dist/migrations/taskMigrations.test.js +0 -34
- package/dist/server/routes.billing.js +0 -991
- package/dist/server/routes.billing.test.js +0 -387
- package/dist/server/routes.sync.integration.test.js +0 -470
- package/dist/server/routes.sync.js +0 -1141
- package/dist/soak/syncSoakHarness.d.ts +0 -6
- package/dist/soak/syncSoakHarness.js +0 -10
- package/dist/ui/assets/index-0W5vyCpn.css +0 -1
- package/dist/ui/assets/index-MCZuNNfn.js +0 -23
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
- package/dist/utils/taskHierarchy.d.ts +0 -4
- package/dist/utils/taskHierarchy.js +0 -69
- package/dist/utils/taskHierarchy.test.js +0 -38
- package/dist/utils/taskParenting.d.ts +0 -3
- package/dist/utils/taskParenting.js +0 -24
- package/dist/utils/taskParenting.test.js +0 -21
- package/dist/utils/taskRelations.d.ts +0 -6
- package/dist/utils/taskRelations.js +0 -24
- package/dist/utils/taskRelations.test.js +0 -34
- package/scripts/check-compat-boundaries.js +0 -48
- /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
- /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
- /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
- /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
- /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
- /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
- /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
- /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
- /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
- /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
- /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
- /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
- /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
- /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
- /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
- /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
|
@@ -0,0 +1,3293 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createTaskforceMcpServer } from './runtime';
|
|
6
|
+
import { MCP_TOOL_REGISTRY } from './toolRegistry';
|
|
7
|
+
import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce';
|
|
8
|
+
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore';
|
|
9
|
+
import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, APP_GATE_DEFINITIONS, } from '../runtime/appGates';
|
|
10
|
+
function createProjectRoot() {
|
|
11
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-runtime-'));
|
|
12
|
+
const core = new TaskforceCore({
|
|
13
|
+
projectRoot,
|
|
14
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
Object.defineProperty(core, 'getAccountAccessStatus', {
|
|
18
|
+
configurable: true,
|
|
19
|
+
value: () => ({
|
|
20
|
+
gate: 'ok',
|
|
21
|
+
hasEntitlement: true,
|
|
22
|
+
canAccessApp: true,
|
|
23
|
+
canAccessPlans: true,
|
|
24
|
+
signupMonetizationStrategy: 'none',
|
|
25
|
+
planSelectionRequired: false,
|
|
26
|
+
message: null,
|
|
27
|
+
entitlement: null
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
31
|
+
core.ensureBootstrapUserAccess({
|
|
32
|
+
userId: 'owner-1',
|
|
33
|
+
workspaceId: 'workspace-1',
|
|
34
|
+
role: 'owner',
|
|
35
|
+
email: 'owner-1@example.com'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
core.close();
|
|
40
|
+
}
|
|
41
|
+
return projectRoot;
|
|
42
|
+
}
|
|
43
|
+
async function listTools(projectRoot, runtimeMode) {
|
|
44
|
+
const runtime = createTaskforceMcpServer({
|
|
45
|
+
projectRoot,
|
|
46
|
+
runtimeMode,
|
|
47
|
+
logStartup: false,
|
|
48
|
+
auth: runtimeMode === 'cloud'
|
|
49
|
+
? { scopes: ['tasks:read', 'tasks:write', 'workspace:read'] }
|
|
50
|
+
: undefined
|
|
51
|
+
});
|
|
52
|
+
const handler = runtime.server._requestHandlers.get('tools/list');
|
|
53
|
+
if (!handler) {
|
|
54
|
+
throw new Error('tools/list handler not registered');
|
|
55
|
+
}
|
|
56
|
+
return await handler({
|
|
57
|
+
jsonrpc: '2.0',
|
|
58
|
+
id: 'test-tools-list',
|
|
59
|
+
method: 'tools/list',
|
|
60
|
+
params: {}
|
|
61
|
+
}, {});
|
|
62
|
+
}
|
|
63
|
+
async function callTool(projectRoot, runtimeMode, name, args, options) {
|
|
64
|
+
const runtime = createTaskforceMcpServer({
|
|
65
|
+
projectRoot,
|
|
66
|
+
runtimeMode,
|
|
67
|
+
workspaceId: options?.workspaceId,
|
|
68
|
+
logStartup: false,
|
|
69
|
+
auth: runtimeMode === 'cloud'
|
|
70
|
+
? {
|
|
71
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
72
|
+
userId: options?.authUserId || 'owner-1',
|
|
73
|
+
actorType: 'oauth',
|
|
74
|
+
aiProfileToken: options?.authAiProfileToken || null,
|
|
75
|
+
aiProfileName: options?.authAiProfileName || null,
|
|
76
|
+
aiProfileDescription: options?.authAiProfileDescription || null,
|
|
77
|
+
aiProfileRole: options?.authAiProfileRole || null,
|
|
78
|
+
aiProfileProvider: options?.authAiProfileProvider || null,
|
|
79
|
+
aiProfileModel: options?.authAiProfileModel || null,
|
|
80
|
+
aiProfileSurfaceType: options?.authAiProfileSurfaceType || null,
|
|
81
|
+
}
|
|
82
|
+
: undefined
|
|
83
|
+
});
|
|
84
|
+
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
85
|
+
if (!handler) {
|
|
86
|
+
throw new Error('tools/call handler not registered');
|
|
87
|
+
}
|
|
88
|
+
if (name !== 'resolve_profile' && options?.resolveProfile !== false && MCP_TOOL_REGISTRY[name]?.mutating) {
|
|
89
|
+
await handler({
|
|
90
|
+
jsonrpc: '2.0',
|
|
91
|
+
id: 'test-tools-call-bootstrap-resolve-profile',
|
|
92
|
+
method: 'tools/call',
|
|
93
|
+
params: {
|
|
94
|
+
name: 'resolve_profile',
|
|
95
|
+
arguments: {
|
|
96
|
+
name: 'Codex',
|
|
97
|
+
icon: 'Bot',
|
|
98
|
+
color: '#ef6c00'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, {});
|
|
102
|
+
}
|
|
103
|
+
return await handler({
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
id: `test-tools-call-${name}`,
|
|
106
|
+
method: 'tools/call',
|
|
107
|
+
params: {
|
|
108
|
+
name,
|
|
109
|
+
arguments: args
|
|
110
|
+
}
|
|
111
|
+
}, {});
|
|
112
|
+
}
|
|
113
|
+
async function readResource(projectRoot, runtimeMode, uri, options) {
|
|
114
|
+
const runtime = createTaskforceMcpServer({
|
|
115
|
+
projectRoot,
|
|
116
|
+
runtimeMode,
|
|
117
|
+
workspaceId: options?.workspaceId,
|
|
118
|
+
logStartup: false,
|
|
119
|
+
auth: runtimeMode === 'cloud'
|
|
120
|
+
? {
|
|
121
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
122
|
+
userId: options?.authUserId || 'owner-1',
|
|
123
|
+
actorType: 'oauth'
|
|
124
|
+
}
|
|
125
|
+
: undefined
|
|
126
|
+
});
|
|
127
|
+
const handler = runtime.server._requestHandlers.get('resources/read');
|
|
128
|
+
if (!handler) {
|
|
129
|
+
throw new Error('resources/read handler not registered');
|
|
130
|
+
}
|
|
131
|
+
return await handler({
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
id: `test-resources-read-${uri}`,
|
|
134
|
+
method: 'resources/read',
|
|
135
|
+
params: { uri }
|
|
136
|
+
}, {});
|
|
137
|
+
}
|
|
138
|
+
async function listResources(projectRoot, runtimeMode, options) {
|
|
139
|
+
const runtime = createTaskforceMcpServer({
|
|
140
|
+
projectRoot,
|
|
141
|
+
runtimeMode,
|
|
142
|
+
workspaceId: options?.workspaceId,
|
|
143
|
+
logStartup: false,
|
|
144
|
+
auth: runtimeMode === 'cloud'
|
|
145
|
+
? {
|
|
146
|
+
scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
|
|
147
|
+
userId: options?.authUserId || 'owner-1',
|
|
148
|
+
actorType: 'oauth'
|
|
149
|
+
}
|
|
150
|
+
: undefined
|
|
151
|
+
});
|
|
152
|
+
const handler = runtime.server._requestHandlers.get('resources/list');
|
|
153
|
+
if (!handler) {
|
|
154
|
+
throw new Error('resources/list handler not registered');
|
|
155
|
+
}
|
|
156
|
+
return await handler({
|
|
157
|
+
jsonrpc: '2.0',
|
|
158
|
+
id: 'test-resources-list',
|
|
159
|
+
method: 'resources/list',
|
|
160
|
+
params: {}
|
|
161
|
+
}, {});
|
|
162
|
+
}
|
|
163
|
+
function getTool(result, name) {
|
|
164
|
+
const tool = result.tools.find((entry) => entry.name === name);
|
|
165
|
+
expect(tool).toBeDefined();
|
|
166
|
+
return tool;
|
|
167
|
+
}
|
|
168
|
+
function parseToolJson(result) {
|
|
169
|
+
const text = result.content?.[0]?.text;
|
|
170
|
+
expect(text).toBeTruthy();
|
|
171
|
+
return JSON.parse(String(text));
|
|
172
|
+
}
|
|
173
|
+
function seedAnnotatedAttachmentFixture(projectRoot) {
|
|
174
|
+
const core = new TaskforceCore({
|
|
175
|
+
projectRoot,
|
|
176
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
const category = core.getCategories()[0]?.value;
|
|
180
|
+
const type = core.getTypes()[0]?.value;
|
|
181
|
+
const task = core.createTask({
|
|
182
|
+
title: 'Annotated attachment fixture',
|
|
183
|
+
category,
|
|
184
|
+
type
|
|
185
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
186
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
187
|
+
assetStore.upsertAsset({
|
|
188
|
+
assetId: 'asset-annotated-1',
|
|
189
|
+
workspaceId: 'workspace-1',
|
|
190
|
+
kind: 'image',
|
|
191
|
+
logicalName: 'Fixture image',
|
|
192
|
+
originalFilename: 'fixture.png',
|
|
193
|
+
mimeType: 'image/png',
|
|
194
|
+
storageProvider: 'local',
|
|
195
|
+
storageKey: 'workspaces/workspace-1/assets/images/asset-annotated-1-fixture.png',
|
|
196
|
+
contentSha256: 'sha-annotated-1',
|
|
197
|
+
sizeBytes: 512,
|
|
198
|
+
});
|
|
199
|
+
assetStore.upsertLink({
|
|
200
|
+
workspaceId: 'workspace-1',
|
|
201
|
+
assetId: 'asset-annotated-1',
|
|
202
|
+
taskId: task.id,
|
|
203
|
+
role: 'attachment',
|
|
204
|
+
});
|
|
205
|
+
const session = core.createAnnotatedAttachmentSession({
|
|
206
|
+
workspaceId: 'workspace-1',
|
|
207
|
+
taskId: task.id,
|
|
208
|
+
baseImageAssetId: 'asset-annotated-1',
|
|
209
|
+
title: 'Fixture session',
|
|
210
|
+
globalInstruction: 'Answer each annotation in order.',
|
|
211
|
+
actorId: 'owner-1',
|
|
212
|
+
annotations: [
|
|
213
|
+
{
|
|
214
|
+
id: 'annotation-1',
|
|
215
|
+
kind: 'pin',
|
|
216
|
+
order: 0,
|
|
217
|
+
markerType: 'question',
|
|
218
|
+
instruction: 'What should change here?',
|
|
219
|
+
x: 0.25,
|
|
220
|
+
y: 0.4,
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
return { task, session };
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
core.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function seedDocumentReviewFixture(projectRoot) {
|
|
231
|
+
const core = new TaskforceCore({
|
|
232
|
+
projectRoot,
|
|
233
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
237
|
+
const { basePath } = core.getPaths();
|
|
238
|
+
const storageKey = 'workspaces/workspace-1/assets/documents/asset-document-1-fixture-plan.md';
|
|
239
|
+
const absolutePath = path.join(basePath, storageKey);
|
|
240
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
241
|
+
fs.writeFileSync(absolutePath, '# Fixture plan\n\n## Rollout\n\nPlease tighten the rollout section.\n', 'utf8');
|
|
242
|
+
const asset = assetStore.upsertAsset({
|
|
243
|
+
assetId: 'asset-document-1',
|
|
244
|
+
workspaceId: 'workspace-1',
|
|
245
|
+
kind: 'document',
|
|
246
|
+
logicalName: 'Fixture plan',
|
|
247
|
+
originalFilename: 'fixture-plan.md',
|
|
248
|
+
mimeType: 'text/markdown',
|
|
249
|
+
storageProvider: 'local',
|
|
250
|
+
storageKey,
|
|
251
|
+
contentSha256: 'sha-document-1',
|
|
252
|
+
sizeBytes: 768,
|
|
253
|
+
documentId: 'doc-fixture-1',
|
|
254
|
+
referenceNumber: 123,
|
|
255
|
+
version: 2,
|
|
256
|
+
isLatest: true,
|
|
257
|
+
});
|
|
258
|
+
const session = core.createDocumentReviewSession({
|
|
259
|
+
workspaceId: 'workspace-1',
|
|
260
|
+
assetId: asset.assetId,
|
|
261
|
+
title: 'Fixture review',
|
|
262
|
+
actorId: 'owner-1',
|
|
263
|
+
});
|
|
264
|
+
const commented = core.addDocumentReviewComment({
|
|
265
|
+
workspaceId: 'workspace-1',
|
|
266
|
+
sessionId: session.id,
|
|
267
|
+
body: 'Please tighten the rollout section.',
|
|
268
|
+
anchor: { kind: 'line-range', startLine: 18, endLine: 24 },
|
|
269
|
+
actorId: 'owner-1',
|
|
270
|
+
});
|
|
271
|
+
return { asset, session: commented };
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
core.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
describe('Taskforce MCP tool descriptions', () => {
|
|
278
|
+
it('keeps the runtime tool list aligned with the shared registry', async () => {
|
|
279
|
+
const projectRoot = createProjectRoot();
|
|
280
|
+
try {
|
|
281
|
+
const result = await listTools(projectRoot, 'local');
|
|
282
|
+
const runtimeToolNames = result.tools.map((tool) => tool.name).sort();
|
|
283
|
+
const registryToolNames = Object.keys(MCP_TOOL_REGISTRY).sort();
|
|
284
|
+
expect(runtimeToolNames).toEqual(registryToolNames);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
it('keeps the cloud runtime tool list aligned with cloud-visible registry tools', async () => {
|
|
291
|
+
const projectRoot = createProjectRoot();
|
|
292
|
+
try {
|
|
293
|
+
const result = await listTools(projectRoot, 'cloud');
|
|
294
|
+
const runtimeToolNames = result.tools.map((tool) => tool.name).sort();
|
|
295
|
+
const registryToolNames = Object.entries(MCP_TOOL_REGISTRY)
|
|
296
|
+
.filter(([, entry]) => entry.permission.kind !== 'local-only' && entry.permission.kind !== 'session-only')
|
|
297
|
+
.map(([name]) => name)
|
|
298
|
+
.sort();
|
|
299
|
+
expect(runtimeToolNames).toEqual(registryToolNames);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
it('includes local-only guidance in local runtime tool descriptions', async () => {
|
|
306
|
+
const projectRoot = createProjectRoot();
|
|
307
|
+
try {
|
|
308
|
+
const result = await listTools(projectRoot, 'local');
|
|
309
|
+
const listTasks = getTool(result, 'list_tasks');
|
|
310
|
+
const getTask = getTool(result, 'get_task');
|
|
311
|
+
const getPaths = getTool(result, 'get_paths');
|
|
312
|
+
const searchImages = getTool(result, 'search_images');
|
|
313
|
+
const getDocument = getTool(result, 'get_document');
|
|
314
|
+
const searchDocuments = getTool(result, 'search_documents');
|
|
315
|
+
const listAnnotatedAttachmentSessions = getTool(result, 'get_annotated_attachment_sessions');
|
|
316
|
+
const readAnnotatedAttachmentPayload = getTool(result, 'read_annotated_attachment_payload');
|
|
317
|
+
const createAnnotatedAttachmentSession = getTool(result, 'create_annotated_attachment_session');
|
|
318
|
+
const deleteAnnotatedAttachmentSession = getTool(result, 'delete_annotated_attachment_session');
|
|
319
|
+
const reviewImage = getTool(result, 'review_image');
|
|
320
|
+
const addAnnotatedAttachmentMarker = getTool(result, 'add_annotated_attachment_marker');
|
|
321
|
+
const bulkAddAnnotatedAttachmentMarkers = getTool(result, 'bulk_add_annotated_attachment_markers');
|
|
322
|
+
const reorderAnnotatedAttachmentMarker = getTool(result, 'reorder_annotated_attachment_marker');
|
|
323
|
+
const getDocumentReviewSessions = getTool(result, 'get_document_review_sessions');
|
|
324
|
+
const addDocumentReviewComment = getTool(result, 'add_document_review_comment');
|
|
325
|
+
const updateDocumentReviewComment = getTool(result, 'update_document_review_comment');
|
|
326
|
+
expect(listTasks.description).toContain('Use format=json for programmatic use.');
|
|
327
|
+
expect(listTasks.description).toContain('NOTE: Use this tool instead of editing files directly to ensure data integrity.');
|
|
328
|
+
expect(getTask.description).toContain('Do not use this for image references like I-123 or document references like D-123.');
|
|
329
|
+
expect(getPaths.description).toContain('Get project and Taskforce storage paths.');
|
|
330
|
+
expect(searchImages.description).toContain('I-123 reference');
|
|
331
|
+
expect(getDocument.description).toContain('D-123');
|
|
332
|
+
expect(searchDocuments.description).toContain('D-123 reference');
|
|
333
|
+
expect(listAnnotatedAttachmentSessions.description).toContain('saved annotation sessions');
|
|
334
|
+
expect(readAnnotatedAttachmentPayload.description).toContain('derived crop hints');
|
|
335
|
+
expect(createAnnotatedAttachmentSession.description).toContain('before adding markers');
|
|
336
|
+
expect(deleteAnnotatedAttachmentSession.description).toContain('rather than deleting markers one by one');
|
|
337
|
+
expect(reviewImage.description).toContain('first-pass visual review');
|
|
338
|
+
expect(addAnnotatedAttachmentMarker.description).toContain('normalized 0..1 coordinates');
|
|
339
|
+
expect(bulkAddAnnotatedAttachmentMarkers.description).toContain('first-pass review');
|
|
340
|
+
expect(reorderAnnotatedAttachmentMarker.description).toContain('review order');
|
|
341
|
+
expect(getDocumentReviewSessions.description).toContain('D-123');
|
|
342
|
+
expect(addDocumentReviewComment.description).toContain('attached to the document rather than the task');
|
|
343
|
+
expect(updateDocumentReviewComment.description).toContain('without creating a new comment');
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
it('omits local-file guidance in cloud runtime tool descriptions while keeping richer tool selection hints', async () => {
|
|
350
|
+
const projectRoot = createProjectRoot();
|
|
351
|
+
try {
|
|
352
|
+
const result = await listTools(projectRoot, 'cloud');
|
|
353
|
+
const allDescriptions = result.tools.map((tool) => tool.description).join('\n');
|
|
354
|
+
const getConfig = getTool(result, 'get_config');
|
|
355
|
+
const searchTasks = getTool(result, 'search_tasks');
|
|
356
|
+
const getTask = getTool(result, 'get_task');
|
|
357
|
+
const searchImages = getTool(result, 'search_images');
|
|
358
|
+
const getImage = getTool(result, 'get_image');
|
|
359
|
+
const searchDocuments = getTool(result, 'search_documents');
|
|
360
|
+
const getDocument = getTool(result, 'get_document');
|
|
361
|
+
const readDocumentContent = getTool(result, 'read_document_content');
|
|
362
|
+
const getDocumentReviewSessions = getTool(result, 'get_document_review_sessions');
|
|
363
|
+
const getCurrentProfile = getTool(result, 'get_current_profile');
|
|
364
|
+
const createDocumentReviewSession = getTool(result, 'create_document_review_session');
|
|
365
|
+
const updateDocumentReviewSession = getTool(result, 'update_document_review_session');
|
|
366
|
+
const reviewDocument = getTool(result, 'review_document');
|
|
367
|
+
const addDocumentReviewComment = getTool(result, 'add_document_review_comment');
|
|
368
|
+
const getTaskAttachments = getTool(result, 'get_task_attachments');
|
|
369
|
+
const readTaskAttachment = getTool(result, 'read_task_attachment');
|
|
370
|
+
const deleteTaskAttachment = getTool(result, 'delete_task_attachment');
|
|
371
|
+
const deleteAnnotatedAttachmentSession = getTool(result, 'delete_annotated_attachment_session');
|
|
372
|
+
const archiveTask = getTool(result, 'archive_task');
|
|
373
|
+
const startTask = getTool(result, 'start_task');
|
|
374
|
+
const finishTask = getTool(result, 'finish_task');
|
|
375
|
+
const updateTask = getTool(result, 'update_task');
|
|
376
|
+
const assignTask = getTool(result, 'assign_task');
|
|
377
|
+
const bulkAssignTasks = getTool(result, 'bulk_assign_tasks');
|
|
378
|
+
const listAssignees = getTool(result, 'list_assignees');
|
|
379
|
+
const linkTaskContext = getTool(result, 'link_task_context');
|
|
380
|
+
const saveTaskAttachment = getTool(result, 'save_task_attachment');
|
|
381
|
+
const resolveProfile = getTool(result, 'resolve_profile');
|
|
382
|
+
expect(allDescriptions).not.toContain('NOTE: Use this tool instead of editing files directly to ensure data integrity.');
|
|
383
|
+
expect(getConfig.description).toContain('Call this at the start of a session or before create_task/update_task');
|
|
384
|
+
expect(searchTasks.description).toContain('Prefer this over list_tasks when looking for a specific task by name.');
|
|
385
|
+
expect(getTask.description).toContain('does not return attachment file contents');
|
|
386
|
+
expect(getTask.description).toContain('Do not fetch attachment path URLs directly');
|
|
387
|
+
expect(getTask.description).toContain('Do not use this for image references like I-123 or document references like D-123.');
|
|
388
|
+
expect(searchImages.description).toContain('get_image');
|
|
389
|
+
expect(getImage.description).toContain('Use this first when the user references an image');
|
|
390
|
+
expect(searchDocuments.description).toContain('get_document');
|
|
391
|
+
expect(searchDocuments.description).toContain('read_document_content');
|
|
392
|
+
expect(getDocument.description).toContain('Use this first when the user references a document');
|
|
393
|
+
expect(getDocument.description).toContain('read_document_content');
|
|
394
|
+
expect(getCurrentProfile.description).toContain('current profile id');
|
|
395
|
+
expect(getCurrentProfile.description).toContain('assigned to itself');
|
|
396
|
+
expect(readDocumentContent.description).toContain('inspect what it is reviewing');
|
|
397
|
+
expect(readDocumentContent.description).toContain('before adding review comments');
|
|
398
|
+
expect(getDocumentReviewSessions.description).toContain('read_document_content');
|
|
399
|
+
expect(createDocumentReviewSession.description).toContain('Requires AI identity via resolve_profile');
|
|
400
|
+
expect(updateDocumentReviewSession.description).toContain('Requires AI identity via resolve_profile');
|
|
401
|
+
expect(reviewDocument.description).toContain('lower-friction document review path');
|
|
402
|
+
expect(reviewDocument.description).toContain('Requires AI identity via resolve_profile');
|
|
403
|
+
expect(addDocumentReviewComment.description).toContain('Requires AI identity via resolve_profile');
|
|
404
|
+
expect(deleteAnnotatedAttachmentSession.description).toContain('whole session was created by mistake');
|
|
405
|
+
expect(getTaskAttachments.description).toContain('use read_task_attachment for file contents');
|
|
406
|
+
expect(getTaskAttachments.description).toContain('prefer get_document and read_document_content');
|
|
407
|
+
expect(readTaskAttachment.description).toContain('Prefer this tool over opening attachment path URLs directly');
|
|
408
|
+
expect(readTaskAttachment.description).toContain('canonical document');
|
|
409
|
+
expect(deleteTaskAttachment.description).toContain('Use get_task_attachments first');
|
|
410
|
+
expect(archiveTask.description).toContain('active tasks may still use status=done for final lookover');
|
|
411
|
+
expect(startTask.description).toContain('preferred AI start-of-task workflow');
|
|
412
|
+
expect(startTask.description).toContain('will not steal a task from a different assignee');
|
|
413
|
+
expect(finishTask.description).toContain("Use outcome='done' when work is complete");
|
|
414
|
+
expect(finishTask.description).toContain('default end-of-task handoff');
|
|
415
|
+
expect(finishTask.description).toContain('outcome=\'review\' when human validation or approval is needed');
|
|
416
|
+
expect(finishTask.description).toContain('either finish it with this tool or explain why it remains open');
|
|
417
|
+
expect(finishTask.description).toContain('This tool never archives automatically');
|
|
418
|
+
expect(updateTask.description).toContain('Use start_task when beginning active work on a tracked task');
|
|
419
|
+
expect(updateTask.description).toContain('prefer finish_task for the final completion or review handoff');
|
|
420
|
+
expect(assignTask.description).toContain('For assigning 3 or more tasks at once, use bulk_assign_tasks instead.');
|
|
421
|
+
expect(bulkAssignTasks.description).toContain('Preferred over repeated assign_task calls');
|
|
422
|
+
expect(listAssignees.description).toContain('List eligible assignees for the active workspace.');
|
|
423
|
+
expect(linkTaskContext.description).toContain('external http(s) URL');
|
|
424
|
+
expect(saveTaskAttachment.description).toContain('generated content');
|
|
425
|
+
expect(saveTaskAttachment.description).toContain('overwrite=true');
|
|
426
|
+
expect(resolveProfile.description).toContain('AI collaborator profile');
|
|
427
|
+
expect(resolveProfile.description).toContain('Update semantics');
|
|
428
|
+
expect(resolveProfile.description).toContain('empty strings are ignored rather than clearing');
|
|
429
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType).toBeDefined();
|
|
430
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('self-declared AI surface type');
|
|
431
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('agent for a persistent named AI collaborator in Taskforce');
|
|
432
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('coding_tool for the coding surface or terminal tool itself');
|
|
433
|
+
expect(resolveProfile.inputSchema?.properties?.surfaceType?.description).toContain('If you are unsure and you are registering a named collaborator');
|
|
434
|
+
const surfaceTypeSchema = resolveProfile.inputSchema?.properties?.surfaceType;
|
|
435
|
+
expect(surfaceTypeSchema?.anyOf?.[0]?.enum).toEqual([
|
|
436
|
+
'chat_app',
|
|
437
|
+
'ide',
|
|
438
|
+
'coding_tool',
|
|
439
|
+
'agent',
|
|
440
|
+
'other'
|
|
441
|
+
]);
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
it('creates and re-resolves ai profiles through resolve_profile', async () => {
|
|
448
|
+
const projectRoot = createProjectRoot();
|
|
449
|
+
try {
|
|
450
|
+
const first = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
451
|
+
name: 'Codex',
|
|
452
|
+
icon: 'Bot',
|
|
453
|
+
color: '#ef6c00',
|
|
454
|
+
description: 'Builds features and fixes regressions',
|
|
455
|
+
role: 'Implementer',
|
|
456
|
+
provider: 'OpenAI',
|
|
457
|
+
model: 'gpt-5.4',
|
|
458
|
+
surfaceType: 'coding_tool'
|
|
459
|
+
}));
|
|
460
|
+
expect(first.created).toBe(true);
|
|
461
|
+
expect(first.profileToken).toMatch(/^tfp_/);
|
|
462
|
+
expect(first.profile.id).toMatch(/^ai-profile-/);
|
|
463
|
+
expect(first.profile.profileToken).toMatch(/^tfp_/);
|
|
464
|
+
expect(first.profileToken).toBe(first.profile.profileToken);
|
|
465
|
+
expect(first.profile.description).toBe('Builds features and fixes regressions');
|
|
466
|
+
expect(first.profile.role).toBe('Implementer');
|
|
467
|
+
expect(first.profile.provider).toBe('OpenAI');
|
|
468
|
+
expect(first.profile.model).toBe('gpt-5.4');
|
|
469
|
+
expect(first.profile.surfaceType).toBe('coding_tool');
|
|
470
|
+
const second = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
471
|
+
name: 'Ignored Rename',
|
|
472
|
+
description: '',
|
|
473
|
+
surfaceType: null,
|
|
474
|
+
profileToken: first.profile.profileToken
|
|
475
|
+
}));
|
|
476
|
+
expect(second.created).toBe(false);
|
|
477
|
+
expect(second.profileToken).toBe(first.profile.profileToken);
|
|
478
|
+
expect(second.profile.id).toBe(first.profile.id);
|
|
479
|
+
expect(second.profile.name).toBe('Ignored Rename');
|
|
480
|
+
expect(second.profile.description).toBe('Builds features and fixes regressions');
|
|
481
|
+
expect(second.profile.surfaceType).toBeNull();
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
it('updates profile metadata when resolve_profile reuses an existing exact-match profile without a token', async () => {
|
|
488
|
+
const projectRoot = createProjectRoot();
|
|
489
|
+
try {
|
|
490
|
+
const first = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
491
|
+
name: 'John Drake',
|
|
492
|
+
icon: 'Bot',
|
|
493
|
+
color: '#64748b'
|
|
494
|
+
}));
|
|
495
|
+
expect(first.created).toBe(true);
|
|
496
|
+
expect(first.profile.description).toBeNull();
|
|
497
|
+
expect(first.profile.role).toBeNull();
|
|
498
|
+
expect(first.profile.provider).toBeNull();
|
|
499
|
+
expect(first.profile.model).toBeNull();
|
|
500
|
+
const second = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
501
|
+
name: 'John Drake',
|
|
502
|
+
icon: '🕴️',
|
|
503
|
+
color: '#2F3A4A',
|
|
504
|
+
description: 'AI collaborator for Taskforce',
|
|
505
|
+
role: 'Advisor',
|
|
506
|
+
provider: 'OpenClaw',
|
|
507
|
+
model: 'gpt-5.4',
|
|
508
|
+
surfaceType: 'agent'
|
|
509
|
+
}));
|
|
510
|
+
expect(second.created).toBe(false);
|
|
511
|
+
expect(second.profile.id).toBe(first.profile.id);
|
|
512
|
+
expect(second.profile.icon).toBe('🕴️');
|
|
513
|
+
expect(second.profile.color).toBe('#2F3A4A');
|
|
514
|
+
expect(second.profile.description).toBe('AI collaborator for Taskforce');
|
|
515
|
+
expect(second.profile.role).toBe('Advisor');
|
|
516
|
+
expect(second.profile.provider).toBe('OpenClaw');
|
|
517
|
+
expect(second.profile.model).toBe('gpt-5.4');
|
|
518
|
+
expect(second.profile.surfaceType).toBe('agent');
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
it('allows read-only MCP tools before resolve_profile', async () => {
|
|
525
|
+
const projectRoot = createProjectRoot();
|
|
526
|
+
try {
|
|
527
|
+
const core = new TaskforceCore({
|
|
528
|
+
projectRoot,
|
|
529
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
530
|
+
});
|
|
531
|
+
const seeded = core.createTask({ title: 'Readable before profile' }, { workspaceId: 'workspace-1' });
|
|
532
|
+
core.close();
|
|
533
|
+
const config = parseToolJson(await callTool(projectRoot, 'local', 'get_config', {}));
|
|
534
|
+
const task = parseToolJson(await callTool(projectRoot, 'local', 'get_task', { id: seeded.referenceLabel }));
|
|
535
|
+
expect(Array.isArray(config.categories)).toBe(true);
|
|
536
|
+
expect(task.id).toBe(seeded.id);
|
|
537
|
+
expect(task.title).toBe('Readable before profile');
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
it('uses the active workspace taxonomy for get_config and create_task', async () => {
|
|
544
|
+
const projectRoot = createProjectRoot();
|
|
545
|
+
try {
|
|
546
|
+
const core = new TaskforceCore({
|
|
547
|
+
projectRoot,
|
|
548
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
549
|
+
});
|
|
550
|
+
core.createWorkspace({ workspaceId: 'workspace-2', name: 'Workspace 2' });
|
|
551
|
+
core.ensureBootstrapUserAccess({
|
|
552
|
+
userId: 'owner-1',
|
|
553
|
+
workspaceId: 'workspace-2',
|
|
554
|
+
role: 'owner',
|
|
555
|
+
email: 'owner-1@example.com'
|
|
556
|
+
});
|
|
557
|
+
core.updateCategories([{ value: 'default', label: 'Default' }], 'workspace-2');
|
|
558
|
+
core.updateTypes([
|
|
559
|
+
{ value: 'feature', label: 'Feature' },
|
|
560
|
+
{ value: 'bug', label: 'Bug' },
|
|
561
|
+
{ value: 'chore', label: 'Chore' }
|
|
562
|
+
], 'workspace-2');
|
|
563
|
+
core.updatePriorities([
|
|
564
|
+
{ value: 1, label: 'Low' },
|
|
565
|
+
{ value: 2, label: 'Medium' },
|
|
566
|
+
{ value: 3, label: 'High' },
|
|
567
|
+
{ value: 4, label: 'Critical' }
|
|
568
|
+
], 'workspace-2');
|
|
569
|
+
core.close();
|
|
570
|
+
const config = parseToolJson(await callTool(projectRoot, 'cloud', 'get_config', {}, {
|
|
571
|
+
workspaceId: 'workspace-2'
|
|
572
|
+
}));
|
|
573
|
+
expect(config.types.map((type) => type.value)).toEqual(['feature', 'bug', 'chore']);
|
|
574
|
+
expect(config.priorities.map((priority) => priority.value)).toEqual([1, 2, 3, 4]);
|
|
575
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
576
|
+
title: 'Workspace scoped task',
|
|
577
|
+
type: 'feature',
|
|
578
|
+
priority: 4
|
|
579
|
+
}, {
|
|
580
|
+
workspaceId: 'workspace-2'
|
|
581
|
+
}));
|
|
582
|
+
expect(created.type).toBe('feature');
|
|
583
|
+
expect(created.priority).toBe(4);
|
|
584
|
+
const createdWithDefaultType = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
585
|
+
title: 'Workspace scoped default type'
|
|
586
|
+
}, {
|
|
587
|
+
workspaceId: 'workspace-2'
|
|
588
|
+
}));
|
|
589
|
+
expect(createdWithDefaultType.type).toBe('feature');
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
it('returns structured taxonomy confirmation details for recoverable MCP errors', async () => {
|
|
596
|
+
const { buildRecoverableToolErrorResult } = await import('./runtime');
|
|
597
|
+
const result = buildRecoverableToolErrorResult(new TaskforceRuleError('Task contains unresolved taxonomy values. Confirmation is required before create.', 409, 'TAXONOMY_NEEDS_CONFIRMATION', {
|
|
598
|
+
needs_confirmation: true,
|
|
599
|
+
unresolved: [{ field: 'type', submitted: 'deliverable', suggestions: [{ value: 'feature', label: 'Feature', confidence: 0.9 }] }],
|
|
600
|
+
normalized_retry_payload: { title: 'Needs taxonomy confirmation', type: 'feature', category: 'default' }
|
|
601
|
+
}));
|
|
602
|
+
const payload = parseToolJson(result);
|
|
603
|
+
expect(payload.success).toBe(false);
|
|
604
|
+
expect(payload.code).toBe('TAXONOMY_NEEDS_CONFIRMATION');
|
|
605
|
+
expect(payload.needs_confirmation).toBe(true);
|
|
606
|
+
expect(payload.details).toEqual(expect.objectContaining({
|
|
607
|
+
needs_confirmation: true,
|
|
608
|
+
unresolved: expect.arrayContaining([
|
|
609
|
+
expect.objectContaining({
|
|
610
|
+
field: 'type',
|
|
611
|
+
submitted: 'deliverable'
|
|
612
|
+
})
|
|
613
|
+
]),
|
|
614
|
+
normalized_retry_payload: expect.objectContaining({
|
|
615
|
+
title: 'Needs taxonomy confirmation',
|
|
616
|
+
type: 'feature',
|
|
617
|
+
category: 'default'
|
|
618
|
+
})
|
|
619
|
+
}));
|
|
620
|
+
});
|
|
621
|
+
it('blocks mutating MCP tools until resolve_profile is called', async () => {
|
|
622
|
+
const projectRoot = createProjectRoot();
|
|
623
|
+
try {
|
|
624
|
+
const localResult = await callTool(projectRoot, 'local', 'create_task', {
|
|
625
|
+
title: 'Should not be created'
|
|
626
|
+
}, { resolveProfile: false });
|
|
627
|
+
expect(localResult.isError).toBe(true);
|
|
628
|
+
expect(localResult.content?.[0]?.text).toContain('Call resolve_profile first');
|
|
629
|
+
expect(localResult.content?.[0]?.text).toContain('pass profileToken on mutating calls');
|
|
630
|
+
const localPayload = parseToolJson(localResult);
|
|
631
|
+
expect(localPayload.code).toBe('AI_PROFILE_REQUIRED');
|
|
632
|
+
expect(localPayload.nextTool).toBe('resolve_profile');
|
|
633
|
+
expect(localPayload.retryHint).toContain('profileToken');
|
|
634
|
+
const cloudResult = await callTool(projectRoot, 'cloud', 'create_task', {
|
|
635
|
+
title: 'Should not be created in cloud either'
|
|
636
|
+
}, { resolveProfile: false });
|
|
637
|
+
expect(cloudResult.isError).toBe(true);
|
|
638
|
+
expect(cloudResult.content?.[0]?.text).toContain('Call resolve_profile first');
|
|
639
|
+
const after = new TaskforceCore({
|
|
640
|
+
projectRoot,
|
|
641
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
642
|
+
});
|
|
643
|
+
try {
|
|
644
|
+
const tasks = after.listTasks('workspace-1').tasks;
|
|
645
|
+
expect(tasks.some((task) => task.title === 'Should not be created')).toBe(false);
|
|
646
|
+
expect(tasks.some((task) => task.title === 'Should not be created in cloud either')).toBe(false);
|
|
647
|
+
}
|
|
648
|
+
finally {
|
|
649
|
+
after.close();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
it('auto-binds a configured cloud AI name for mutating tools', async () => {
|
|
657
|
+
const projectRoot = createProjectRoot();
|
|
658
|
+
try {
|
|
659
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
660
|
+
title: 'Automation bootstrap task'
|
|
661
|
+
}, {
|
|
662
|
+
workspaceId: 'workspace-1',
|
|
663
|
+
resolveProfile: false,
|
|
664
|
+
authAiProfileName: 'Automation Codex'
|
|
665
|
+
}));
|
|
666
|
+
expect(created.title).toBe('Automation bootstrap task');
|
|
667
|
+
expect(created.createdBy).toMatch(/^ai-profile-/);
|
|
668
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
669
|
+
label: 'Automation Codex',
|
|
670
|
+
kind: 'ai'
|
|
671
|
+
}));
|
|
672
|
+
expect(created.assignee).toBe('unassigned');
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
it('persists configured cloud AI metadata during auto-bind bootstrap', async () => {
|
|
679
|
+
const projectRoot = createProjectRoot();
|
|
680
|
+
try {
|
|
681
|
+
await callTool(projectRoot, 'cloud', 'create_task', {
|
|
682
|
+
title: 'Metadata bootstrap task'
|
|
683
|
+
}, {
|
|
684
|
+
workspaceId: 'workspace-1',
|
|
685
|
+
resolveProfile: false,
|
|
686
|
+
authAiProfileName: 'Automation Codex',
|
|
687
|
+
authAiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
|
|
688
|
+
authAiProfileRole: 'Coding Agent',
|
|
689
|
+
authAiProfileProvider: 'OpenAI',
|
|
690
|
+
authAiProfileModel: 'Codex',
|
|
691
|
+
authAiProfileSurfaceType: 'coding_tool'
|
|
692
|
+
});
|
|
693
|
+
const currentProfile = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
|
|
694
|
+
workspaceId: 'workspace-1',
|
|
695
|
+
resolveProfile: false,
|
|
696
|
+
authAiProfileName: 'Automation Codex',
|
|
697
|
+
authAiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
|
|
698
|
+
authAiProfileRole: 'Coding Agent',
|
|
699
|
+
authAiProfileProvider: 'OpenAI',
|
|
700
|
+
authAiProfileModel: 'Codex',
|
|
701
|
+
authAiProfileSurfaceType: 'coding_tool'
|
|
702
|
+
}));
|
|
703
|
+
expect(currentProfile.profile).toEqual(expect.objectContaining({
|
|
704
|
+
name: 'Automation Codex',
|
|
705
|
+
description: 'OpenAI Codex coding agent connected through MCP.',
|
|
706
|
+
role: 'Coding Agent',
|
|
707
|
+
provider: 'OpenAI',
|
|
708
|
+
model: 'Codex',
|
|
709
|
+
surfaceType: 'coding_tool'
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
it('allows mutating MCP tools in a fresh runtime when profileToken is passed', async () => {
|
|
717
|
+
const projectRoot = createProjectRoot();
|
|
718
|
+
try {
|
|
719
|
+
const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
720
|
+
name: 'Codex',
|
|
721
|
+
icon: 'Bot',
|
|
722
|
+
color: '#ef6c00'
|
|
723
|
+
}, { resolveProfile: false }));
|
|
724
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
725
|
+
title: 'Stateless profile token write',
|
|
726
|
+
profileToken: resolved.profile.profileToken,
|
|
727
|
+
}, { resolveProfile: false }));
|
|
728
|
+
expect(created.title).toBe('Stateless profile token write');
|
|
729
|
+
expect(created.createdBy).toBe(resolved.profile.id);
|
|
730
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
731
|
+
id: resolved.profile.id,
|
|
732
|
+
username: resolved.profile.username
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
finally {
|
|
736
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
it('allows mutating cloud MCP tools when the connection is configured with an AI profile token', async () => {
|
|
740
|
+
const projectRoot = createProjectRoot();
|
|
741
|
+
try {
|
|
742
|
+
const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
743
|
+
name: 'Automation Codex'
|
|
744
|
+
}, { resolveProfile: false }));
|
|
745
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
746
|
+
title: 'Configured token write'
|
|
747
|
+
}, {
|
|
748
|
+
workspaceId: 'workspace-1',
|
|
749
|
+
resolveProfile: false,
|
|
750
|
+
authAiProfileToken: resolved.profile.profileToken
|
|
751
|
+
}));
|
|
752
|
+
expect(created.createdBy).toBe(resolved.profile.id);
|
|
753
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
754
|
+
id: resolved.profile.id,
|
|
755
|
+
username: resolved.profile.username
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
it('recovers from a stale configured cloud AI profile token when a valid AI name is provided', async () => {
|
|
763
|
+
const projectRoot = createProjectRoot();
|
|
764
|
+
try {
|
|
765
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
766
|
+
title: 'Recovered automation write'
|
|
767
|
+
}, {
|
|
768
|
+
workspaceId: 'workspace-1',
|
|
769
|
+
resolveProfile: false,
|
|
770
|
+
authAiProfileToken: 'tfp_stale_automation_token',
|
|
771
|
+
authAiProfileName: 'Automation Codex'
|
|
772
|
+
}));
|
|
773
|
+
expect(created.title).toBe('Recovered automation write');
|
|
774
|
+
expect(created.createdBy).toMatch(/^ai-profile-/);
|
|
775
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
776
|
+
label: 'Automation Codex',
|
|
777
|
+
kind: 'ai'
|
|
778
|
+
}));
|
|
779
|
+
const currentProfile = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
|
|
780
|
+
workspaceId: 'workspace-1',
|
|
781
|
+
resolveProfile: false,
|
|
782
|
+
authAiProfileToken: 'tfp_stale_automation_token',
|
|
783
|
+
authAiProfileName: 'Automation Codex'
|
|
784
|
+
}));
|
|
785
|
+
expect(currentProfile.identityBound).toBe(true);
|
|
786
|
+
expect(currentProfile.profileToken).toMatch(/^tfp_/);
|
|
787
|
+
expect(currentProfile.profile).toEqual(expect.objectContaining({
|
|
788
|
+
name: 'Automation Codex',
|
|
789
|
+
profileToken: expect.stringMatching(/^tfp_/)
|
|
790
|
+
}));
|
|
791
|
+
expect(currentProfile.profileToken).toBe(currentProfile.profile.profileToken);
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
it('rejects invalid profileToken values on mutating MCP tools', async () => {
|
|
798
|
+
const projectRoot = createProjectRoot();
|
|
799
|
+
try {
|
|
800
|
+
const result = await callTool(projectRoot, 'local', 'create_task', {
|
|
801
|
+
title: 'Invalid token write',
|
|
802
|
+
profileToken: 'tfp_invalid_token',
|
|
803
|
+
}, { resolveProfile: false });
|
|
804
|
+
expect(result.isError).toBe(true);
|
|
805
|
+
expect(result.content?.[0]?.text).toContain('Invalid AI profile token');
|
|
806
|
+
const payload = parseToolJson(result);
|
|
807
|
+
expect(payload.code).toBe('INVALID_AI_PROFILE_TOKEN');
|
|
808
|
+
expect(payload.nextTool).toBe('resolve_profile');
|
|
809
|
+
expect(payload.retryHint).toContain('fresh profileToken');
|
|
810
|
+
}
|
|
811
|
+
finally {
|
|
812
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
it('recovers from an invalid per-call profileToken when the connection has a valid AI name', async () => {
|
|
816
|
+
const projectRoot = createProjectRoot();
|
|
817
|
+
try {
|
|
818
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_task', {
|
|
819
|
+
title: 'Recovered per-call token write',
|
|
820
|
+
profileToken: 'tfp_invalid_token',
|
|
821
|
+
}, {
|
|
822
|
+
workspaceId: 'workspace-1',
|
|
823
|
+
resolveProfile: false,
|
|
824
|
+
authAiProfileName: 'Automation Codex'
|
|
825
|
+
}));
|
|
826
|
+
expect(created.title).toBe('Recovered per-call token write');
|
|
827
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
828
|
+
label: 'Automation Codex',
|
|
829
|
+
kind: 'ai'
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
finally {
|
|
833
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
it('claims unassigned tasks and marks them in progress through start_task', async () => {
|
|
837
|
+
const projectRoot = createProjectRoot();
|
|
838
|
+
try {
|
|
839
|
+
const core = new TaskforceCore({
|
|
840
|
+
projectRoot,
|
|
841
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
842
|
+
});
|
|
843
|
+
const seeded = core.createTask({
|
|
844
|
+
title: 'Start helper unassigned path',
|
|
845
|
+
status: 'task',
|
|
846
|
+
assignee: 'unassigned'
|
|
847
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
848
|
+
core.close();
|
|
849
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
850
|
+
id: seeded.referenceLabel
|
|
851
|
+
}));
|
|
852
|
+
expect(result.started).toBe(true);
|
|
853
|
+
expect(result.claimed).toBe(true);
|
|
854
|
+
expect(result.statusChanged).toBe(true);
|
|
855
|
+
expect(result.previousAssignee).toBe('unassigned');
|
|
856
|
+
expect(result.previousStatus).toBe('task');
|
|
857
|
+
expect(result.task.status).toBe('in-progress');
|
|
858
|
+
expect(result.task.assignee).toMatch(/^ai-profile-/);
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
it('sets tasks already assigned to the current AI in progress without stealing ownership', async () => {
|
|
865
|
+
const projectRoot = createProjectRoot();
|
|
866
|
+
try {
|
|
867
|
+
const profile = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
|
|
868
|
+
name: 'Codex',
|
|
869
|
+
icon: 'Bot',
|
|
870
|
+
color: '#ef6c00'
|
|
871
|
+
}));
|
|
872
|
+
const core = new TaskforceCore({
|
|
873
|
+
projectRoot,
|
|
874
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
875
|
+
});
|
|
876
|
+
const seeded = core.createTask({
|
|
877
|
+
title: 'Start helper owned path',
|
|
878
|
+
status: 'task',
|
|
879
|
+
assignee: profile.profile.id
|
|
880
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
881
|
+
core.close();
|
|
882
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'start_task', {
|
|
883
|
+
id: seeded.referenceLabel
|
|
884
|
+
}));
|
|
885
|
+
expect(result.started).toBe(true);
|
|
886
|
+
expect(result.claimed).toBe(false);
|
|
887
|
+
expect(result.statusChanged).toBe(true);
|
|
888
|
+
expect(result.previousAssignee).toBe(profile.profile.id);
|
|
889
|
+
expect(result.task.assignee).toBe(profile.profile.id);
|
|
890
|
+
expect(result.task.status).toBe('in-progress');
|
|
891
|
+
}
|
|
892
|
+
finally {
|
|
893
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
it('refuses to steal tasks already assigned to someone else through start_task', async () => {
|
|
897
|
+
const projectRoot = createProjectRoot();
|
|
898
|
+
try {
|
|
899
|
+
const core = new TaskforceCore({
|
|
900
|
+
projectRoot,
|
|
901
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
902
|
+
});
|
|
903
|
+
const seeded = core.createTask({
|
|
904
|
+
title: 'Start helper assignee refusal',
|
|
905
|
+
status: 'task',
|
|
906
|
+
assignee: 'owner-1'
|
|
907
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
908
|
+
core.close();
|
|
909
|
+
const result = await callTool(projectRoot, 'local', 'start_task', {
|
|
910
|
+
id: seeded.referenceLabel
|
|
911
|
+
});
|
|
912
|
+
expect(result.isError).toBe(true);
|
|
913
|
+
expect(result.content?.[0]?.text).toContain('will not steal work from another assignee');
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
it('rejects terminal tasks through start_task', async () => {
|
|
920
|
+
const projectRoot = createProjectRoot();
|
|
921
|
+
try {
|
|
922
|
+
const core = new TaskforceCore({
|
|
923
|
+
projectRoot,
|
|
924
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
925
|
+
});
|
|
926
|
+
const seeded = core.createTask({
|
|
927
|
+
title: 'Start helper terminal refusal',
|
|
928
|
+
status: 'done',
|
|
929
|
+
assignee: 'unassigned'
|
|
930
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
931
|
+
core.close();
|
|
932
|
+
const result = await callTool(projectRoot, 'local', 'start_task', {
|
|
933
|
+
id: seeded.referenceLabel
|
|
934
|
+
});
|
|
935
|
+
expect(result.isError).toBe(true);
|
|
936
|
+
expect(result.content?.[0]?.text).toContain('must be reopened explicitly');
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
it('requires AI identity for start_task when bootstrap is disabled', async () => {
|
|
943
|
+
const projectRoot = createProjectRoot();
|
|
944
|
+
try {
|
|
945
|
+
const core = new TaskforceCore({
|
|
946
|
+
projectRoot,
|
|
947
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
948
|
+
});
|
|
949
|
+
const seeded = core.createTask({
|
|
950
|
+
title: 'Start helper identity guard',
|
|
951
|
+
status: 'task',
|
|
952
|
+
assignee: 'unassigned'
|
|
953
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
954
|
+
core.close();
|
|
955
|
+
const result = await callTool(projectRoot, 'local', 'start_task', {
|
|
956
|
+
id: seeded.referenceLabel
|
|
957
|
+
}, { resolveProfile: false });
|
|
958
|
+
expect(result.isError).toBe(true);
|
|
959
|
+
const payload = parseToolJson(result);
|
|
960
|
+
expect(payload.code).toBe('AI_PROFILE_REQUIRED');
|
|
961
|
+
expect(payload.nextTool).toBe('resolve_profile');
|
|
962
|
+
}
|
|
963
|
+
finally {
|
|
964
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
it('marks completed tasks done without archiving them through finish_task', async () => {
|
|
968
|
+
const projectRoot = createProjectRoot();
|
|
969
|
+
try {
|
|
970
|
+
const core = new TaskforceCore({
|
|
971
|
+
projectRoot,
|
|
972
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
973
|
+
});
|
|
974
|
+
const seeded = core.createTask({
|
|
975
|
+
title: 'Finish helper done path',
|
|
976
|
+
status: 'in-progress'
|
|
977
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
978
|
+
core.close();
|
|
979
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'finish_task', {
|
|
980
|
+
id: seeded.referenceLabel,
|
|
981
|
+
outcome: 'done',
|
|
982
|
+
summary: 'Implementation completed'
|
|
983
|
+
}));
|
|
984
|
+
expect(result.task.status).toBe('done');
|
|
985
|
+
expect(result.completion).toEqual(expect.objectContaining({
|
|
986
|
+
outcome: 'done',
|
|
987
|
+
archived: false,
|
|
988
|
+
summary: 'Implementation completed'
|
|
989
|
+
}));
|
|
990
|
+
const after = new TaskforceCore({
|
|
991
|
+
projectRoot,
|
|
992
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
expect(after.listTasks('workspace-1').tasks.some((task) => task.id === seeded.id && task.status === 'done')).toBe(true);
|
|
996
|
+
expect(after.listArchive('workspace-1').archived.some((task) => task.id === seeded.id)).toBe(false);
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
after.close();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
finally {
|
|
1003
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
it('hands review tasks to the current human user through finish_task', async () => {
|
|
1007
|
+
const projectRoot = createProjectRoot();
|
|
1008
|
+
try {
|
|
1009
|
+
const core = new TaskforceCore({
|
|
1010
|
+
projectRoot,
|
|
1011
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1012
|
+
});
|
|
1013
|
+
core.ensureBootstrapUserAccess({
|
|
1014
|
+
userId: 'reviewer-1',
|
|
1015
|
+
workspaceId: 'workspace-1',
|
|
1016
|
+
role: 'member',
|
|
1017
|
+
email: 'reviewer-1@example.com'
|
|
1018
|
+
});
|
|
1019
|
+
const seeded = core.createTask({
|
|
1020
|
+
title: 'Finish helper review path',
|
|
1021
|
+
status: 'in-progress',
|
|
1022
|
+
assignee: 'unassigned'
|
|
1023
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1024
|
+
core.close();
|
|
1025
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'finish_task', {
|
|
1026
|
+
id: seeded.referenceLabel,
|
|
1027
|
+
outcome: 'review'
|
|
1028
|
+
}, { authUserId: 'reviewer-1' }));
|
|
1029
|
+
expect(result.task.status).toBe('review');
|
|
1030
|
+
expect(result.task.assignee).toBe('reviewer-1');
|
|
1031
|
+
expect(result.completion).toEqual(expect.objectContaining({
|
|
1032
|
+
outcome: 'review',
|
|
1033
|
+
archived: false,
|
|
1034
|
+
reviewerResolution: expect.objectContaining({
|
|
1035
|
+
assignee: 'reviewer-1',
|
|
1036
|
+
source: 'current_user'
|
|
1037
|
+
})
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
1040
|
+
finally {
|
|
1041
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
it('preserves an existing human owner when handing off a task for review', async () => {
|
|
1045
|
+
const projectRoot = createProjectRoot();
|
|
1046
|
+
try {
|
|
1047
|
+
const core = new TaskforceCore({
|
|
1048
|
+
projectRoot,
|
|
1049
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1050
|
+
});
|
|
1051
|
+
core.ensureBootstrapUserAccess({
|
|
1052
|
+
userId: 'reviewer-2',
|
|
1053
|
+
workspaceId: 'workspace-1',
|
|
1054
|
+
role: 'member',
|
|
1055
|
+
email: 'reviewer-2@example.com'
|
|
1056
|
+
});
|
|
1057
|
+
const seeded = core.createTask({
|
|
1058
|
+
title: 'Preserve existing human owner',
|
|
1059
|
+
status: 'in-progress',
|
|
1060
|
+
assignee: 'reviewer-2'
|
|
1061
|
+
}, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
1062
|
+
core.close();
|
|
1063
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'finish_task', {
|
|
1064
|
+
id: seeded.referenceLabel,
|
|
1065
|
+
outcome: 'review'
|
|
1066
|
+
}, { authUserId: 'owner-1' }));
|
|
1067
|
+
expect(result.task.status).toBe('review');
|
|
1068
|
+
expect(result.task.assignee).toBe('reviewer-2');
|
|
1069
|
+
expect(result.completion.reviewerResolution).toEqual(expect.objectContaining({
|
|
1070
|
+
assignee: 'reviewer-2',
|
|
1071
|
+
source: 'existing_human_owner'
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
it('lists eligible assignee ids in MCP tool schemas instead of generic agent guidance', async () => {
|
|
1079
|
+
const projectRoot = createProjectRoot();
|
|
1080
|
+
try {
|
|
1081
|
+
const core = new TaskforceCore({
|
|
1082
|
+
projectRoot,
|
|
1083
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1084
|
+
});
|
|
1085
|
+
core.ensureBootstrapUserAccess({
|
|
1086
|
+
userId: 'member-2',
|
|
1087
|
+
workspaceId: 'workspace-1',
|
|
1088
|
+
role: 'member',
|
|
1089
|
+
email: 'member-2@example.com'
|
|
1090
|
+
});
|
|
1091
|
+
core.resolveAiProfile({
|
|
1092
|
+
workspaceId: 'workspace-1',
|
|
1093
|
+
name: 'Codex',
|
|
1094
|
+
icon: 'Bot',
|
|
1095
|
+
color: '#ef6c00'
|
|
1096
|
+
});
|
|
1097
|
+
core.close();
|
|
1098
|
+
const result = await listTools(projectRoot, 'local');
|
|
1099
|
+
const assignTask = getTool(result, 'assign_task');
|
|
1100
|
+
const createTask = getTool(result, 'create_task');
|
|
1101
|
+
expect(assignTask.inputSchema?.properties?.assignee?.description).toContain('member-2');
|
|
1102
|
+
expect(assignTask.inputSchema?.properties?.assignee?.description).toContain('Codex');
|
|
1103
|
+
expect(assignTask.inputSchema?.properties?.assignee?.description).not.toContain("Use 'agent'");
|
|
1104
|
+
expect(createTask.inputSchema?.properties?.profileToken?.description).toContain('does not preserve session identity');
|
|
1105
|
+
}
|
|
1106
|
+
finally {
|
|
1107
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
it('returns eligible assignees and marks the current resolved AI profile', async () => {
|
|
1111
|
+
const projectRoot = createProjectRoot();
|
|
1112
|
+
try {
|
|
1113
|
+
const core = new TaskforceCore({
|
|
1114
|
+
projectRoot,
|
|
1115
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1116
|
+
});
|
|
1117
|
+
core.ensureBootstrapUserAccess({
|
|
1118
|
+
userId: 'member-2',
|
|
1119
|
+
workspaceId: 'workspace-1',
|
|
1120
|
+
role: 'member',
|
|
1121
|
+
email: 'member-2@example.com'
|
|
1122
|
+
});
|
|
1123
|
+
core.close();
|
|
1124
|
+
const runtime = createTaskforceMcpServer({
|
|
1125
|
+
projectRoot,
|
|
1126
|
+
runtimeMode: 'local',
|
|
1127
|
+
logStartup: false
|
|
1128
|
+
});
|
|
1129
|
+
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
1130
|
+
if (!handler)
|
|
1131
|
+
throw new Error('tools/call handler not registered');
|
|
1132
|
+
const resolved = parseToolJson(await handler({
|
|
1133
|
+
jsonrpc: '2.0',
|
|
1134
|
+
id: 'test-tools-call-resolve-profile',
|
|
1135
|
+
method: 'tools/call',
|
|
1136
|
+
params: {
|
|
1137
|
+
name: 'resolve_profile',
|
|
1138
|
+
arguments: {
|
|
1139
|
+
name: 'Codex',
|
|
1140
|
+
icon: 'Bot',
|
|
1141
|
+
color: '#ef6c00',
|
|
1142
|
+
description: 'Builds and repairs product code',
|
|
1143
|
+
role: 'Implementer',
|
|
1144
|
+
provider: 'OpenAI',
|
|
1145
|
+
model: 'gpt-5.4'
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}, {}));
|
|
1149
|
+
const payload = parseToolJson(await handler({
|
|
1150
|
+
jsonrpc: '2.0',
|
|
1151
|
+
id: 'test-tools-call-list-assignees',
|
|
1152
|
+
method: 'tools/call',
|
|
1153
|
+
params: {
|
|
1154
|
+
name: 'list_assignees',
|
|
1155
|
+
arguments: {}
|
|
1156
|
+
}
|
|
1157
|
+
}, {}));
|
|
1158
|
+
expect(payload.workspaceId).toBe('workspace-1');
|
|
1159
|
+
expect(payload.assignees).toEqual(expect.arrayContaining([
|
|
1160
|
+
expect.objectContaining({ id: 'owner-1', kind: 'member', username: 'owner-1@example.com' }),
|
|
1161
|
+
expect.objectContaining({ id: 'member-2', kind: 'member', username: 'member-2@example.com' }),
|
|
1162
|
+
expect.objectContaining({
|
|
1163
|
+
id: resolved.profile.id,
|
|
1164
|
+
kind: 'agent',
|
|
1165
|
+
username: resolved.profile.username,
|
|
1166
|
+
description: 'Builds and repairs product code',
|
|
1167
|
+
role: 'Implementer',
|
|
1168
|
+
provider: 'OpenAI',
|
|
1169
|
+
model: 'gpt-5.4',
|
|
1170
|
+
isCurrentProfile: true
|
|
1171
|
+
})
|
|
1172
|
+
]));
|
|
1173
|
+
}
|
|
1174
|
+
finally {
|
|
1175
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
it('returns the current AI profile and suggested assignee filter', async () => {
|
|
1179
|
+
const projectRoot = createProjectRoot();
|
|
1180
|
+
try {
|
|
1181
|
+
const runtime = createTaskforceMcpServer({
|
|
1182
|
+
projectRoot,
|
|
1183
|
+
runtimeMode: 'local',
|
|
1184
|
+
logStartup: false
|
|
1185
|
+
});
|
|
1186
|
+
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
1187
|
+
if (!handler)
|
|
1188
|
+
throw new Error('tools/call handler not registered');
|
|
1189
|
+
const resolved = parseToolJson(await handler({
|
|
1190
|
+
jsonrpc: '2.0',
|
|
1191
|
+
id: 'test-tools-call-resolve-profile-current',
|
|
1192
|
+
method: 'tools/call',
|
|
1193
|
+
params: {
|
|
1194
|
+
name: 'resolve_profile',
|
|
1195
|
+
arguments: {
|
|
1196
|
+
name: 'Codex',
|
|
1197
|
+
icon: 'Bot',
|
|
1198
|
+
color: '#ef6c00'
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}, {}));
|
|
1202
|
+
const payload = parseToolJson(await handler({
|
|
1203
|
+
jsonrpc: '2.0',
|
|
1204
|
+
id: 'test-tools-call-get-current-profile',
|
|
1205
|
+
method: 'tools/call',
|
|
1206
|
+
params: {
|
|
1207
|
+
name: 'get_current_profile',
|
|
1208
|
+
arguments: {}
|
|
1209
|
+
}
|
|
1210
|
+
}, {}));
|
|
1211
|
+
expect(payload.identityBound).toBe(true);
|
|
1212
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
1213
|
+
id: resolved.profile.id,
|
|
1214
|
+
username: resolved.profile.username,
|
|
1215
|
+
profileToken: resolved.profile.profileToken,
|
|
1216
|
+
}));
|
|
1217
|
+
expect(payload.taskFilter).toEqual({ assignee: resolved.profile.id });
|
|
1218
|
+
expect(payload.nextTool).toBe('list_tasks');
|
|
1219
|
+
expect(payload.suggestedArgs).toEqual({ assignee: resolved.profile.id, format: 'json' });
|
|
1220
|
+
expect(payload.guidance).toContain('suggestedArgs');
|
|
1221
|
+
}
|
|
1222
|
+
finally {
|
|
1223
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
it('guides agents to resolve_profile when no current AI profile is bound', async () => {
|
|
1227
|
+
const projectRoot = createProjectRoot();
|
|
1228
|
+
try {
|
|
1229
|
+
const payload = parseToolJson(await callTool(projectRoot, 'local', 'get_current_profile', {}, {
|
|
1230
|
+
resolveProfile: false
|
|
1231
|
+
}));
|
|
1232
|
+
expect(payload.identityBound).toBe(false);
|
|
1233
|
+
expect(payload.profile).toBeNull();
|
|
1234
|
+
expect(payload.taskFilter).toBeNull();
|
|
1235
|
+
expect(payload.nextTool).toBe('resolve_profile');
|
|
1236
|
+
expect(payload.suggestedArgs).toEqual({ name: 'Your AI name' });
|
|
1237
|
+
expect(payload.guidance).toContain('Call resolve_profile first');
|
|
1238
|
+
}
|
|
1239
|
+
finally {
|
|
1240
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
it('auto-binds get_current_profile from a configured cloud AI name', async () => {
|
|
1244
|
+
const projectRoot = createProjectRoot();
|
|
1245
|
+
try {
|
|
1246
|
+
const payload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
|
|
1247
|
+
workspaceId: 'workspace-1',
|
|
1248
|
+
resolveProfile: false,
|
|
1249
|
+
authAiProfileName: 'Automation Codex'
|
|
1250
|
+
}));
|
|
1251
|
+
expect(payload.identityBound).toBe(true);
|
|
1252
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
1253
|
+
name: 'Automation Codex',
|
|
1254
|
+
username: 'automation-codex',
|
|
1255
|
+
profileToken: expect.stringMatching(/^tfp_/)
|
|
1256
|
+
}));
|
|
1257
|
+
expect(payload.taskFilter).toEqual({ assignee: payload.profile.id });
|
|
1258
|
+
expect(payload.nextTool).toBe('list_tasks');
|
|
1259
|
+
}
|
|
1260
|
+
finally {
|
|
1261
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
it('searches tasks by T-XXX reference label via reference_number', async () => {
|
|
1265
|
+
const projectRoot = createProjectRoot();
|
|
1266
|
+
try {
|
|
1267
|
+
const core = new TaskforceCore({
|
|
1268
|
+
projectRoot,
|
|
1269
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1270
|
+
});
|
|
1271
|
+
core.createTask({
|
|
1272
|
+
id: 'task-ref-587',
|
|
1273
|
+
referenceNumber: 587,
|
|
1274
|
+
title: 'Reference search target'
|
|
1275
|
+
}, {
|
|
1276
|
+
workspaceId: 'workspace-1',
|
|
1277
|
+
allowClientProvidedId: true
|
|
1278
|
+
});
|
|
1279
|
+
core.close();
|
|
1280
|
+
const result = await callTool(projectRoot, 'cloud', 'search_tasks', { query: 'T-587' }, {
|
|
1281
|
+
workspaceId: 'workspace-1',
|
|
1282
|
+
authUserId: 'owner-1'
|
|
1283
|
+
});
|
|
1284
|
+
expect(result.content?.[0]?.text).toContain('T-587');
|
|
1285
|
+
expect(result.content?.[0]?.text).toContain('Reference search target');
|
|
1286
|
+
}
|
|
1287
|
+
finally {
|
|
1288
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
it('keeps T-XXX lookup errors scoped to the active workspace', async () => {
|
|
1292
|
+
const projectRoot = createProjectRoot();
|
|
1293
|
+
try {
|
|
1294
|
+
const core = new TaskforceCore({
|
|
1295
|
+
projectRoot,
|
|
1296
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1297
|
+
});
|
|
1298
|
+
core.createWorkspace({ workspaceId: 'workspace-2', name: 'Workspace 2' });
|
|
1299
|
+
core.ensureBootstrapUserAccess({
|
|
1300
|
+
userId: 'owner-1',
|
|
1301
|
+
workspaceId: 'workspace-2',
|
|
1302
|
+
role: 'owner',
|
|
1303
|
+
email: 'owner-1@example.com'
|
|
1304
|
+
});
|
|
1305
|
+
core.createTask({
|
|
1306
|
+
title: 'Other workspace task'
|
|
1307
|
+
}, {
|
|
1308
|
+
workspaceId: 'workspace-2'
|
|
1309
|
+
});
|
|
1310
|
+
core.close();
|
|
1311
|
+
const result = await callTool(projectRoot, 'cloud', 'get_task', { id: 'T-1' }, {
|
|
1312
|
+
workspaceId: 'workspace-1',
|
|
1313
|
+
authUserId: 'owner-1'
|
|
1314
|
+
});
|
|
1315
|
+
expect(result.content?.[0]?.text).toContain('Task T-1 not found in workspace workspace-1.');
|
|
1316
|
+
expect(result.content?.[0]?.text).not.toContain('workspace-2');
|
|
1317
|
+
}
|
|
1318
|
+
finally {
|
|
1319
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
it('lists and reads initiative and workstream resources by readable reference', async () => {
|
|
1323
|
+
const projectRoot = createProjectRoot();
|
|
1324
|
+
try {
|
|
1325
|
+
const core = new TaskforceCore({
|
|
1326
|
+
projectRoot,
|
|
1327
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1328
|
+
});
|
|
1329
|
+
const initiative = core.createInitiative({
|
|
1330
|
+
title: 'Planning initiative'
|
|
1331
|
+
}, 'workspace-1');
|
|
1332
|
+
const workstream = core.createWorkstream({
|
|
1333
|
+
title: 'Planning workstream',
|
|
1334
|
+
initiativeId: initiative.id
|
|
1335
|
+
}, 'workspace-1');
|
|
1336
|
+
core.createTask({
|
|
1337
|
+
title: 'Linked planning task',
|
|
1338
|
+
workstreamId: workstream.id
|
|
1339
|
+
}, {
|
|
1340
|
+
workspaceId: 'workspace-1'
|
|
1341
|
+
});
|
|
1342
|
+
const archivedTask = core.createTask({
|
|
1343
|
+
title: 'Archived planning task',
|
|
1344
|
+
workstreamId: workstream.id
|
|
1345
|
+
}, {
|
|
1346
|
+
workspaceId: 'workspace-1'
|
|
1347
|
+
});
|
|
1348
|
+
core.archiveTask(archivedTask.id, false, undefined, 'workspace-1');
|
|
1349
|
+
core.close();
|
|
1350
|
+
const listed = await listResources(projectRoot, 'cloud', {
|
|
1351
|
+
workspaceId: 'workspace-1',
|
|
1352
|
+
authUserId: 'owner-1'
|
|
1353
|
+
});
|
|
1354
|
+
expect(listed.resources.map((resource) => resource.uri)).toEqual(expect.arrayContaining([
|
|
1355
|
+
`taskforce://${path.basename(projectRoot)}/initiatives`,
|
|
1356
|
+
`taskforce://${path.basename(projectRoot)}/workstreams`,
|
|
1357
|
+
]));
|
|
1358
|
+
const initiativeResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/initiatives/${initiative.referenceLabel}`, {
|
|
1359
|
+
workspaceId: 'workspace-1',
|
|
1360
|
+
authUserId: 'owner-1'
|
|
1361
|
+
});
|
|
1362
|
+
const initiativePayload = JSON.parse(String(initiativeResource.contents[0]?.text || '{}'));
|
|
1363
|
+
expect(initiativePayload.initiative.referenceLabel).toBe(initiative.referenceLabel);
|
|
1364
|
+
expect(initiativePayload.taskCount).toBe(2);
|
|
1365
|
+
expect(initiativePayload.workstreams).toEqual(expect.arrayContaining([
|
|
1366
|
+
expect.objectContaining({ id: workstream.id, referenceLabel: workstream.referenceLabel })
|
|
1367
|
+
]));
|
|
1368
|
+
const workstreamResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/workstreams/${workstream.referenceLabel}`, {
|
|
1369
|
+
workspaceId: 'workspace-1',
|
|
1370
|
+
authUserId: 'owner-1'
|
|
1371
|
+
});
|
|
1372
|
+
const workstreamPayload = JSON.parse(String(workstreamResource.contents[0]?.text || '{}'));
|
|
1373
|
+
expect(workstreamPayload.workstream.referenceLabel).toBe(workstream.referenceLabel);
|
|
1374
|
+
expect(workstreamPayload.taskCount).toBe(2);
|
|
1375
|
+
expect(workstreamPayload.tasks).toEqual(expect.arrayContaining([
|
|
1376
|
+
expect.objectContaining({ title: 'Linked planning task', workstreamId: workstream.id }),
|
|
1377
|
+
expect.objectContaining({ title: 'Archived planning task', workstreamId: workstream.id, isArchived: true, status: 'done' })
|
|
1378
|
+
]));
|
|
1379
|
+
}
|
|
1380
|
+
finally {
|
|
1381
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
it('manages planning entities, relationships, and structured checklist tools', async () => {
|
|
1385
|
+
const projectRoot = createProjectRoot();
|
|
1386
|
+
try {
|
|
1387
|
+
const createdInitiative = parseToolJson(await callTool(projectRoot, 'local', 'create_initiative', {
|
|
1388
|
+
title: 'Launch initiative'
|
|
1389
|
+
}));
|
|
1390
|
+
expect(createdInitiative.initiative.referenceLabel).toMatch(/^IN-/);
|
|
1391
|
+
const createdWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'create_workstream', {
|
|
1392
|
+
title: 'Launch workstream',
|
|
1393
|
+
initiativeId: createdInitiative.initiative.referenceLabel
|
|
1394
|
+
}));
|
|
1395
|
+
expect(createdWorkstream.workstream.referenceLabel).toMatch(/^WS-/);
|
|
1396
|
+
expect(createdWorkstream.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
1397
|
+
const createdTask = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1398
|
+
title: 'Task for MCP planning'
|
|
1399
|
+
}));
|
|
1400
|
+
const taskLinked = parseToolJson(await callTool(projectRoot, 'local', 'set_task_workstream', {
|
|
1401
|
+
taskId: createdTask.referenceLabel,
|
|
1402
|
+
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
1403
|
+
}));
|
|
1404
|
+
expect(taskLinked.workstreamId).toBe(createdWorkstream.workstream.id);
|
|
1405
|
+
const checklistReplaced = parseToolJson(await callTool(projectRoot, 'local', 'replace_task_checklist', {
|
|
1406
|
+
taskId: createdTask.referenceLabel,
|
|
1407
|
+
items: [
|
|
1408
|
+
{ title: 'First checklist item', isCompleted: false },
|
|
1409
|
+
{ title: 'Second checklist item', isCompleted: true }
|
|
1410
|
+
]
|
|
1411
|
+
}));
|
|
1412
|
+
expect(checklistReplaced.items).toHaveLength(2);
|
|
1413
|
+
const checklistRead = parseToolJson(await callTool(projectRoot, 'local', 'get_task_checklist', {
|
|
1414
|
+
taskId: createdTask.referenceLabel
|
|
1415
|
+
}));
|
|
1416
|
+
expect(checklistRead.items.map((item) => item.title)).toEqual(['First checklist item', 'Second checklist item']);
|
|
1417
|
+
const workstreamCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_workstream_initiative', {
|
|
1418
|
+
workstreamId: createdWorkstream.workstream.referenceLabel
|
|
1419
|
+
}));
|
|
1420
|
+
expect(workstreamCleared.workstream.initiativeId).toBeNull();
|
|
1421
|
+
const workstreamReset = parseToolJson(await callTool(projectRoot, 'local', 'set_workstream_initiative', {
|
|
1422
|
+
workstreamId: createdWorkstream.workstream.referenceLabel,
|
|
1423
|
+
initiativeId: createdInitiative.initiative.referenceLabel
|
|
1424
|
+
}));
|
|
1425
|
+
expect(workstreamReset.workstream.initiativeId).toBe(createdInitiative.initiative.id);
|
|
1426
|
+
const archivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'archive_workstream', {
|
|
1427
|
+
id: createdWorkstream.workstream.referenceLabel
|
|
1428
|
+
}));
|
|
1429
|
+
expect(archivedWorkstream.workstream.isArchived).toBe(true);
|
|
1430
|
+
const unarchivedWorkstream = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_workstream', {
|
|
1431
|
+
id: createdWorkstream.workstream.referenceLabel
|
|
1432
|
+
}));
|
|
1433
|
+
expect(unarchivedWorkstream.workstream.isArchived).toBe(false);
|
|
1434
|
+
const archivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'archive_initiative', {
|
|
1435
|
+
id: createdInitiative.initiative.referenceLabel
|
|
1436
|
+
}));
|
|
1437
|
+
expect(archivedInitiative.initiative.isArchived).toBe(true);
|
|
1438
|
+
const unarchivedInitiative = parseToolJson(await callTool(projectRoot, 'local', 'unarchive_initiative', {
|
|
1439
|
+
id: createdInitiative.initiative.referenceLabel
|
|
1440
|
+
}));
|
|
1441
|
+
expect(unarchivedInitiative.initiative.isArchived).toBe(false);
|
|
1442
|
+
const taskCleared = parseToolJson(await callTool(projectRoot, 'local', 'clear_task_workstream', {
|
|
1443
|
+
taskId: createdTask.referenceLabel
|
|
1444
|
+
}));
|
|
1445
|
+
expect(taskCleared.workstreamId).toBeNull();
|
|
1446
|
+
}
|
|
1447
|
+
finally {
|
|
1448
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
it('creates MCP tasks as unassigned when no explicit assignee is provided', async () => {
|
|
1452
|
+
const projectRoot = createProjectRoot();
|
|
1453
|
+
try {
|
|
1454
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
|
|
1455
|
+
title: 'Unassigned by default'
|
|
1456
|
+
}));
|
|
1457
|
+
expect(created.assignee).toBe('unassigned');
|
|
1458
|
+
}
|
|
1459
|
+
finally {
|
|
1460
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
it('reads task resources by T-XXX reference and exposes version/scope in config resource', async () => {
|
|
1464
|
+
const projectRoot = createProjectRoot();
|
|
1465
|
+
try {
|
|
1466
|
+
const core = new TaskforceCore({
|
|
1467
|
+
projectRoot,
|
|
1468
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1469
|
+
});
|
|
1470
|
+
const task = core.createTask({ title: 'Resource reference task' }, { workspaceId: 'workspace-1' });
|
|
1471
|
+
core.close();
|
|
1472
|
+
const taskResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/tasks/${task.referenceLabel}`, {
|
|
1473
|
+
workspaceId: 'workspace-1',
|
|
1474
|
+
authUserId: 'owner-1'
|
|
1475
|
+
});
|
|
1476
|
+
const configResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/config`, {
|
|
1477
|
+
workspaceId: 'workspace-1',
|
|
1478
|
+
authUserId: 'owner-1'
|
|
1479
|
+
});
|
|
1480
|
+
const taskPayload = JSON.parse(String(taskResource.contents[0]?.text || '{}'));
|
|
1481
|
+
const configPayload = JSON.parse(String(configResource.contents[0]?.text || '{}'));
|
|
1482
|
+
expect(taskPayload.id).toBe(task.id);
|
|
1483
|
+
expect(taskPayload.referenceLabel).toBe(task.referenceLabel);
|
|
1484
|
+
expect(configPayload.server?.version).toBeTruthy();
|
|
1485
|
+
expect(configPayload.server?.runtimeMode).toBe('cloud');
|
|
1486
|
+
expect(configPayload.scope?.workspaceId).toBe('workspace-1');
|
|
1487
|
+
}
|
|
1488
|
+
finally {
|
|
1489
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
it('persists save_task_attachment through canonical task attachments', async () => {
|
|
1493
|
+
const projectRoot = createProjectRoot();
|
|
1494
|
+
try {
|
|
1495
|
+
const core = new TaskforceCore({
|
|
1496
|
+
projectRoot,
|
|
1497
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1498
|
+
});
|
|
1499
|
+
const task = core.createTask({
|
|
1500
|
+
title: 'Attachment test'
|
|
1501
|
+
}, {
|
|
1502
|
+
workspaceId: 'workspace-1'
|
|
1503
|
+
});
|
|
1504
|
+
core.close();
|
|
1505
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1506
|
+
id: task.id,
|
|
1507
|
+
filename: 'smoke-test.txt',
|
|
1508
|
+
content: 'hello from runtime test'
|
|
1509
|
+
});
|
|
1510
|
+
const after = new TaskforceCore({
|
|
1511
|
+
projectRoot,
|
|
1512
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1513
|
+
});
|
|
1514
|
+
try {
|
|
1515
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1516
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1517
|
+
const first = updated?.attachments?.[0];
|
|
1518
|
+
expect(typeof first).toBe('object');
|
|
1519
|
+
expect(first.fsPath).toContain('workspaces/workspace-1/assets/files/');
|
|
1520
|
+
}
|
|
1521
|
+
finally {
|
|
1522
|
+
after.close();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
finally {
|
|
1526
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
it('allows save_task_attachment in cloud runtime with inline content', async () => {
|
|
1530
|
+
const projectRoot = createProjectRoot();
|
|
1531
|
+
try {
|
|
1532
|
+
const core = new TaskforceCore({
|
|
1533
|
+
projectRoot,
|
|
1534
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1535
|
+
});
|
|
1536
|
+
const task = core.createTask({ title: 'Cloud attachment test' }, { workspaceId: 'workspace-1' });
|
|
1537
|
+
core.close();
|
|
1538
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1539
|
+
id: task.id,
|
|
1540
|
+
filename: 'cloud-note.md',
|
|
1541
|
+
content: '# Cloud Note'
|
|
1542
|
+
});
|
|
1543
|
+
const after = new TaskforceCore({
|
|
1544
|
+
projectRoot,
|
|
1545
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1546
|
+
});
|
|
1547
|
+
try {
|
|
1548
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1549
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1550
|
+
expect(updated?.attachments?.[0]).toMatchObject({
|
|
1551
|
+
path: expect.stringMatching(/^\/api\/taskforce\/documents\/.+\/content$/),
|
|
1552
|
+
caption: 'cloud-note.md',
|
|
1553
|
+
displayName: 'cloud-note.md',
|
|
1554
|
+
originalFilename: 'cloud-note.md'
|
|
1555
|
+
});
|
|
1556
|
+
expect((updated?.attachments?.[0]).fsPath).toContain('workspaces/workspace-1/assets/documents/');
|
|
1557
|
+
}
|
|
1558
|
+
finally {
|
|
1559
|
+
after.close();
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
finally {
|
|
1563
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
it('allows generic markdown review attachments without requiring the legacy review template', async () => {
|
|
1567
|
+
const projectRoot = createProjectRoot();
|
|
1568
|
+
try {
|
|
1569
|
+
const core = new TaskforceCore({
|
|
1570
|
+
projectRoot,
|
|
1571
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1572
|
+
});
|
|
1573
|
+
const task = core.createTask({ title: 'Generic review attachment test' }, { workspaceId: 'workspace-1' });
|
|
1574
|
+
core.close();
|
|
1575
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1576
|
+
id: task.id,
|
|
1577
|
+
filename: 'review.md',
|
|
1578
|
+
content: ['## Review Verdict', '### Findings', '- Looks good overall.'].join('\n')
|
|
1579
|
+
});
|
|
1580
|
+
const after = new TaskforceCore({
|
|
1581
|
+
projectRoot,
|
|
1582
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1583
|
+
});
|
|
1584
|
+
try {
|
|
1585
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1586
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1587
|
+
expect(updated?.attachments?.[0]).toMatchObject({
|
|
1588
|
+
originalFilename: 'review.md',
|
|
1589
|
+
caption: 'review.md',
|
|
1590
|
+
displayName: 'review.md'
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
finally {
|
|
1594
|
+
after.close();
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
finally {
|
|
1598
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
it('uses the caption as the canonical logical name for markdown attachments', async () => {
|
|
1602
|
+
const projectRoot = createProjectRoot();
|
|
1603
|
+
try {
|
|
1604
|
+
const core = new TaskforceCore({
|
|
1605
|
+
projectRoot,
|
|
1606
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1607
|
+
});
|
|
1608
|
+
const task = core.createTask({ title: 'Canonical markdown naming test' }, { workspaceId: 'workspace-1' });
|
|
1609
|
+
core.close();
|
|
1610
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1611
|
+
id: task.id,
|
|
1612
|
+
filename: 'v2.md',
|
|
1613
|
+
content: '# Plan\n',
|
|
1614
|
+
caption: 'AI Profiles - Implementation Plan v2'
|
|
1615
|
+
});
|
|
1616
|
+
const after = new TaskforceCore({
|
|
1617
|
+
projectRoot,
|
|
1618
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1619
|
+
});
|
|
1620
|
+
try {
|
|
1621
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1622
|
+
expect(updated?.attachments?.[0]).toMatchObject({
|
|
1623
|
+
caption: 'AI Profiles - Implementation Plan v2',
|
|
1624
|
+
displayName: 'AI Profiles - Implementation Plan v2',
|
|
1625
|
+
originalFilename: 'v2.md'
|
|
1626
|
+
});
|
|
1627
|
+
const attachmentAssetId = updated?.attachments?.[0]?.assetId;
|
|
1628
|
+
const store = new WorkspaceAssetStore(after.getDatabaseAdapter(), after.getTenantId());
|
|
1629
|
+
const asset = store.get(String(attachmentAssetId || ''), 'workspace-1');
|
|
1630
|
+
expect(asset).toEqual(expect.objectContaining({
|
|
1631
|
+
logicalName: 'AI Profiles - Implementation Plan v2',
|
|
1632
|
+
originalFilename: 'v2.md'
|
|
1633
|
+
}));
|
|
1634
|
+
expect(store.listLinksForAsset(String(attachmentAssetId || ''), 'workspace-1')).toEqual([
|
|
1635
|
+
expect.objectContaining({
|
|
1636
|
+
taskId: task.id,
|
|
1637
|
+
displayName: null
|
|
1638
|
+
})
|
|
1639
|
+
]);
|
|
1640
|
+
}
|
|
1641
|
+
finally {
|
|
1642
|
+
after.close();
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
finally {
|
|
1646
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1647
|
+
}
|
|
1648
|
+
});
|
|
1649
|
+
it('allows link_task_context in cloud runtime for URLs', async () => {
|
|
1650
|
+
const projectRoot = createProjectRoot();
|
|
1651
|
+
try {
|
|
1652
|
+
const core = new TaskforceCore({
|
|
1653
|
+
projectRoot,
|
|
1654
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1655
|
+
});
|
|
1656
|
+
const task = core.createTask({ title: 'Cloud context link test' }, { workspaceId: 'workspace-1' });
|
|
1657
|
+
core.close();
|
|
1658
|
+
await callTool(projectRoot, 'cloud', 'link_task_context', {
|
|
1659
|
+
id: task.id,
|
|
1660
|
+
path: 'https://example.com/spec',
|
|
1661
|
+
caption: 'Spec'
|
|
1662
|
+
});
|
|
1663
|
+
const after = new TaskforceCore({
|
|
1664
|
+
projectRoot,
|
|
1665
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1666
|
+
});
|
|
1667
|
+
try {
|
|
1668
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1669
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1670
|
+
expect(updated?.attachments?.[0]).toMatchObject({
|
|
1671
|
+
path: 'https://example.com/spec',
|
|
1672
|
+
caption: 'Spec'
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
finally {
|
|
1676
|
+
after.close();
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
finally {
|
|
1680
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
it('rejects local file paths for link_task_context in cloud runtime', async () => {
|
|
1684
|
+
const projectRoot = createProjectRoot();
|
|
1685
|
+
try {
|
|
1686
|
+
const core = new TaskforceCore({
|
|
1687
|
+
projectRoot,
|
|
1688
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1689
|
+
});
|
|
1690
|
+
const task = core.createTask({ title: 'Cloud local-path reject test' }, { workspaceId: 'workspace-1' });
|
|
1691
|
+
core.close();
|
|
1692
|
+
const result = await callTool(projectRoot, 'cloud', 'link_task_context', {
|
|
1693
|
+
id: task.id,
|
|
1694
|
+
path: 'docs/spec.md'
|
|
1695
|
+
});
|
|
1696
|
+
expect(result.content?.[0]?.text).toContain('link_task_context only supports external http(s) URLs');
|
|
1697
|
+
}
|
|
1698
|
+
finally {
|
|
1699
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
it('rejects local file paths for link_task_context in local runtime too', async () => {
|
|
1703
|
+
const projectRoot = createProjectRoot();
|
|
1704
|
+
try {
|
|
1705
|
+
const core = new TaskforceCore({
|
|
1706
|
+
projectRoot,
|
|
1707
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1708
|
+
});
|
|
1709
|
+
const task = core.createTask({ title: 'Local local-path reject test' }, { workspaceId: 'workspace-1' });
|
|
1710
|
+
core.close();
|
|
1711
|
+
const result = await callTool(projectRoot, 'local', 'link_task_context', {
|
|
1712
|
+
id: task.id,
|
|
1713
|
+
path: 'docs/spec.md'
|
|
1714
|
+
});
|
|
1715
|
+
expect(result.content?.[0]?.text).toContain('link_task_context only supports external http(s) URLs');
|
|
1716
|
+
}
|
|
1717
|
+
finally {
|
|
1718
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
it('rejects sourcePath for save_task_attachment in cloud runtime', async () => {
|
|
1722
|
+
const projectRoot = createProjectRoot();
|
|
1723
|
+
try {
|
|
1724
|
+
const core = new TaskforceCore({
|
|
1725
|
+
projectRoot,
|
|
1726
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1727
|
+
});
|
|
1728
|
+
const task = core.createTask({ title: 'Cloud sourcePath reject test' }, { workspaceId: 'workspace-1' });
|
|
1729
|
+
core.close();
|
|
1730
|
+
const result = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1731
|
+
id: task.id,
|
|
1732
|
+
filename: 'copy.txt',
|
|
1733
|
+
sourcePath: 'docs/source.txt'
|
|
1734
|
+
});
|
|
1735
|
+
expect(result.content?.[0]?.text).toContain('Cloud MCP only supports inline content');
|
|
1736
|
+
}
|
|
1737
|
+
finally {
|
|
1738
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
it('accepts task references for save_task_attachment', async () => {
|
|
1742
|
+
const projectRoot = createProjectRoot();
|
|
1743
|
+
try {
|
|
1744
|
+
const core = new TaskforceCore({
|
|
1745
|
+
projectRoot,
|
|
1746
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1747
|
+
});
|
|
1748
|
+
const task = core.createTask({ title: 'Reference attachment save test' }, { workspaceId: 'workspace-1' });
|
|
1749
|
+
core.close();
|
|
1750
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1751
|
+
id: task.referenceLabel,
|
|
1752
|
+
filename: 'reference-note.md',
|
|
1753
|
+
content: '# Saved by reference'
|
|
1754
|
+
});
|
|
1755
|
+
const after = new TaskforceCore({
|
|
1756
|
+
projectRoot,
|
|
1757
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1758
|
+
});
|
|
1759
|
+
try {
|
|
1760
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
1761
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
1762
|
+
expect((updated?.attachments?.[0]).fsPath).toContain('workspaces/workspace-1/assets/documents/');
|
|
1763
|
+
}
|
|
1764
|
+
finally {
|
|
1765
|
+
after.close();
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
finally {
|
|
1769
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
it('overwrites canonical attachments in place when overwrite=true', async () => {
|
|
1773
|
+
const projectRoot = createProjectRoot();
|
|
1774
|
+
try {
|
|
1775
|
+
const core = new TaskforceCore({
|
|
1776
|
+
projectRoot,
|
|
1777
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1778
|
+
});
|
|
1779
|
+
const task = core.createTask({ title: 'Overwrite attachment test' }, { workspaceId: 'workspace-1' });
|
|
1780
|
+
core.close();
|
|
1781
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1782
|
+
id: task.referenceLabel,
|
|
1783
|
+
filename: 'plan.md',
|
|
1784
|
+
content: '# First version'
|
|
1785
|
+
});
|
|
1786
|
+
const before = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1787
|
+
const beforeAttachmentId = before.attachments[0]?.attachmentId;
|
|
1788
|
+
const beforeTimestamp = before.attachments[0]?.timestamp;
|
|
1789
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1790
|
+
id: task.referenceLabel,
|
|
1791
|
+
filename: 'plan.md',
|
|
1792
|
+
content: '# Second version',
|
|
1793
|
+
overwrite: true
|
|
1794
|
+
});
|
|
1795
|
+
const after = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1796
|
+
const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
1797
|
+
taskId: task.referenceLabel,
|
|
1798
|
+
attachmentId: beforeAttachmentId
|
|
1799
|
+
}));
|
|
1800
|
+
expect(after.attachments).toHaveLength(1);
|
|
1801
|
+
expect(after.attachments[0]?.attachmentId).toBe(beforeAttachmentId);
|
|
1802
|
+
expect(after.attachments[0]?.originalFilename).toBe('plan.md');
|
|
1803
|
+
expect(after.attachments[0]?.timestamp).not.toBe(beforeTimestamp);
|
|
1804
|
+
expect(readBack.content).toContain('# Second version');
|
|
1805
|
+
}
|
|
1806
|
+
finally {
|
|
1807
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
it('creates a new attachment when overwrite=true has no canonical filename match', async () => {
|
|
1811
|
+
const projectRoot = createProjectRoot();
|
|
1812
|
+
try {
|
|
1813
|
+
const core = new TaskforceCore({
|
|
1814
|
+
projectRoot,
|
|
1815
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1816
|
+
});
|
|
1817
|
+
const task = core.createTask({ title: 'Overwrite miss test' }, { workspaceId: 'workspace-1' });
|
|
1818
|
+
core.close();
|
|
1819
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1820
|
+
id: task.id,
|
|
1821
|
+
filename: 'first.md',
|
|
1822
|
+
content: '# First'
|
|
1823
|
+
});
|
|
1824
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1825
|
+
id: task.id,
|
|
1826
|
+
filename: 'second.md',
|
|
1827
|
+
content: '# Second',
|
|
1828
|
+
overwrite: true
|
|
1829
|
+
});
|
|
1830
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
1831
|
+
expect(listed.attachments).toHaveLength(2);
|
|
1832
|
+
expect(listed.attachments.map((attachment) => attachment.originalFilename)).toEqual(['first.md', 'second.md']);
|
|
1833
|
+
}
|
|
1834
|
+
finally {
|
|
1835
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
it('does not overwrite external URL attachments when overwrite=true', async () => {
|
|
1839
|
+
const projectRoot = createProjectRoot();
|
|
1840
|
+
try {
|
|
1841
|
+
const core = new TaskforceCore({
|
|
1842
|
+
projectRoot,
|
|
1843
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1844
|
+
});
|
|
1845
|
+
const task = core.createTask({ title: 'External overwrite test' }, { workspaceId: 'workspace-1' });
|
|
1846
|
+
core.close();
|
|
1847
|
+
await callTool(projectRoot, 'local', 'link_task_context', {
|
|
1848
|
+
id: task.id,
|
|
1849
|
+
path: 'https://example.com/plan.md',
|
|
1850
|
+
caption: 'plan.md'
|
|
1851
|
+
});
|
|
1852
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1853
|
+
id: task.id,
|
|
1854
|
+
filename: 'plan.md',
|
|
1855
|
+
content: '# Local plan',
|
|
1856
|
+
overwrite: true
|
|
1857
|
+
});
|
|
1858
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
1859
|
+
expect(listed.attachments).toHaveLength(2);
|
|
1860
|
+
expect(listed.attachments.filter((attachment) => attachment.source === 'external')).toHaveLength(1);
|
|
1861
|
+
expect(listed.attachments.filter((attachment) => attachment.source === 'canonical')).toHaveLength(1);
|
|
1862
|
+
}
|
|
1863
|
+
finally {
|
|
1864
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
it('deletes external URL attachments without touching canonical assets', async () => {
|
|
1868
|
+
const projectRoot = createProjectRoot();
|
|
1869
|
+
try {
|
|
1870
|
+
const core = new TaskforceCore({
|
|
1871
|
+
projectRoot,
|
|
1872
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1873
|
+
});
|
|
1874
|
+
const task = core.createTask({ title: 'Delete external attachment test' }, { workspaceId: 'workspace-1' });
|
|
1875
|
+
core.close();
|
|
1876
|
+
await callTool(projectRoot, 'cloud', 'link_task_context', {
|
|
1877
|
+
id: task.referenceLabel,
|
|
1878
|
+
path: 'https://example.com/spec.md',
|
|
1879
|
+
caption: 'spec.md'
|
|
1880
|
+
});
|
|
1881
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1882
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'delete_task_attachment', {
|
|
1883
|
+
taskId: task.referenceLabel,
|
|
1884
|
+
attachmentId: listed.attachments[0]?.attachmentId
|
|
1885
|
+
}));
|
|
1886
|
+
const after = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1887
|
+
expect(result.success).toBe(true);
|
|
1888
|
+
expect(result.canonicalAssetDeleted).toBe(false);
|
|
1889
|
+
expect(after.attachments).toEqual([]);
|
|
1890
|
+
}
|
|
1891
|
+
finally {
|
|
1892
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
it('deletes canonical task attachments and soft-deletes unattached canonical metadata', async () => {
|
|
1896
|
+
const projectRoot = createProjectRoot();
|
|
1897
|
+
try {
|
|
1898
|
+
const core = new TaskforceCore({
|
|
1899
|
+
projectRoot,
|
|
1900
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1901
|
+
});
|
|
1902
|
+
const task = core.createTask({ title: 'Delete canonical attachment test' }, { workspaceId: 'workspace-1' });
|
|
1903
|
+
core.close();
|
|
1904
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
1905
|
+
id: task.referenceLabel,
|
|
1906
|
+
filename: 'delete-me.md',
|
|
1907
|
+
content: '# Delete me'
|
|
1908
|
+
});
|
|
1909
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1910
|
+
const attachmentId = listed.attachments[0]?.attachmentId;
|
|
1911
|
+
const assetId = listed.attachments[0]?.assetId;
|
|
1912
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'delete_task_attachment', {
|
|
1913
|
+
taskId: task.referenceLabel,
|
|
1914
|
+
attachmentId
|
|
1915
|
+
}));
|
|
1916
|
+
const after = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1917
|
+
const verifyCore = new TaskforceCore({
|
|
1918
|
+
projectRoot,
|
|
1919
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1920
|
+
});
|
|
1921
|
+
try {
|
|
1922
|
+
const store = new WorkspaceAssetStore(verifyCore.getDatabaseAdapter(), verifyCore.getTenantId());
|
|
1923
|
+
const deletedAsset = store.get(String(assetId || ''), 'workspace-1');
|
|
1924
|
+
expect(result.success).toBe(true);
|
|
1925
|
+
expect(result.canonicalAssetDeleted).toBe(true);
|
|
1926
|
+
expect(after.attachments).toEqual([]);
|
|
1927
|
+
expect(deletedAsset?.deletedAt).toBeTruthy();
|
|
1928
|
+
}
|
|
1929
|
+
finally {
|
|
1930
|
+
verifyCore.close();
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
finally {
|
|
1934
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
it('deletes annotated attachment sessions when the last linked image attachment is deleted', async () => {
|
|
1938
|
+
const projectRoot = createProjectRoot();
|
|
1939
|
+
try {
|
|
1940
|
+
const imagePath = path.join(projectRoot, 'fixture-image.png');
|
|
1941
|
+
fs.writeFileSync(imagePath, Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WnHCqgAAAAASUVORK5CYII=', 'base64'));
|
|
1942
|
+
const core = new TaskforceCore({
|
|
1943
|
+
projectRoot,
|
|
1944
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1945
|
+
});
|
|
1946
|
+
const task = core.createTask({ title: 'Delete annotated image attachment test' }, { workspaceId: 'workspace-1' });
|
|
1947
|
+
core.close();
|
|
1948
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
1949
|
+
id: task.referenceLabel,
|
|
1950
|
+
filename: 'fixture-image.png',
|
|
1951
|
+
sourcePath: imagePath
|
|
1952
|
+
});
|
|
1953
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.referenceLabel }));
|
|
1954
|
+
const attachmentId = String(listed.attachments[0]?.attachmentId || '');
|
|
1955
|
+
const assetId = String(listed.attachments[0]?.assetId || '');
|
|
1956
|
+
const seededCore = new TaskforceCore({
|
|
1957
|
+
projectRoot,
|
|
1958
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1959
|
+
});
|
|
1960
|
+
try {
|
|
1961
|
+
seededCore.createAnnotatedAttachmentSession({
|
|
1962
|
+
workspaceId: 'workspace-1',
|
|
1963
|
+
taskId: task.id,
|
|
1964
|
+
baseImageAssetId: assetId,
|
|
1965
|
+
title: 'Image review',
|
|
1966
|
+
annotations: [
|
|
1967
|
+
{ id: 'annotation-1', kind: 'pin', order: 0, instruction: 'Look here', x: 0.3, y: 0.4 },
|
|
1968
|
+
],
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
finally {
|
|
1972
|
+
seededCore.close();
|
|
1973
|
+
}
|
|
1974
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'delete_task_attachment', {
|
|
1975
|
+
taskId: task.referenceLabel,
|
|
1976
|
+
attachmentId
|
|
1977
|
+
}));
|
|
1978
|
+
const verifyCore = new TaskforceCore({
|
|
1979
|
+
projectRoot,
|
|
1980
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
1981
|
+
});
|
|
1982
|
+
try {
|
|
1983
|
+
const activeSessions = verifyCore.listAnnotatedAttachmentSessions({
|
|
1984
|
+
workspaceId: 'workspace-1',
|
|
1985
|
+
taskId: task.id,
|
|
1986
|
+
imageAssetId: assetId,
|
|
1987
|
+
});
|
|
1988
|
+
const allSessions = verifyCore.listAnnotatedAttachmentSessions({
|
|
1989
|
+
workspaceId: 'workspace-1',
|
|
1990
|
+
taskId: task.id,
|
|
1991
|
+
imageAssetId: assetId,
|
|
1992
|
+
includeDeleted: true,
|
|
1993
|
+
});
|
|
1994
|
+
expect(result.success).toBe(true);
|
|
1995
|
+
expect(result.canonicalAssetDeleted).toBe(true);
|
|
1996
|
+
expect(result.deletedAnnotatedSessions).toBeGreaterThanOrEqual(0);
|
|
1997
|
+
expect(activeSessions).toEqual([]);
|
|
1998
|
+
expect(allSessions[0]?.deletedAt).toBeTruthy();
|
|
1999
|
+
}
|
|
2000
|
+
finally {
|
|
2001
|
+
verifyCore.close();
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
finally {
|
|
2005
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2006
|
+
}
|
|
2007
|
+
});
|
|
2008
|
+
it('supports overwrite=true for local sourcePath saves', async () => {
|
|
2009
|
+
const projectRoot = createProjectRoot();
|
|
2010
|
+
try {
|
|
2011
|
+
const core = new TaskforceCore({
|
|
2012
|
+
projectRoot,
|
|
2013
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2014
|
+
});
|
|
2015
|
+
const task = core.createTask({ title: 'Source path overwrite test' }, { workspaceId: 'workspace-1' });
|
|
2016
|
+
core.close();
|
|
2017
|
+
const sourcePath = path.join(projectRoot, 'notes.md');
|
|
2018
|
+
fs.writeFileSync(sourcePath, '# First source', 'utf8');
|
|
2019
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2020
|
+
id: task.referenceLabel,
|
|
2021
|
+
filename: 'notes.md',
|
|
2022
|
+
sourcePath: 'notes.md'
|
|
2023
|
+
});
|
|
2024
|
+
const before = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2025
|
+
fs.writeFileSync(sourcePath, '# Updated source', 'utf8');
|
|
2026
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2027
|
+
id: task.referenceLabel,
|
|
2028
|
+
filename: 'notes.md',
|
|
2029
|
+
sourcePath: 'notes.md',
|
|
2030
|
+
overwrite: true
|
|
2031
|
+
});
|
|
2032
|
+
const after = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2033
|
+
expect(after.attachments).toHaveLength(1);
|
|
2034
|
+
expect(after.attachments[0]?.attachmentId).toBe(before.attachments[0]?.attachmentId);
|
|
2035
|
+
}
|
|
2036
|
+
finally {
|
|
2037
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
it('allows local sourcePath saves from Taskforce-managed tmp/artifact directories', async () => {
|
|
2041
|
+
const projectRoot = createProjectRoot();
|
|
2042
|
+
try {
|
|
2043
|
+
const core = new TaskforceCore({
|
|
2044
|
+
projectRoot,
|
|
2045
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2046
|
+
});
|
|
2047
|
+
const task = core.createTask({ title: 'Taskforce managed source path test' }, { workspaceId: 'workspace-1' });
|
|
2048
|
+
core.close();
|
|
2049
|
+
const managedSourcePath = path.join(projectRoot, '.taskforce', 'tmp', 'generated-note.md');
|
|
2050
|
+
fs.mkdirSync(path.dirname(managedSourcePath), { recursive: true });
|
|
2051
|
+
fs.writeFileSync(managedSourcePath, '# Generated in Taskforce tmp', 'utf8');
|
|
2052
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2053
|
+
id: task.referenceLabel,
|
|
2054
|
+
filename: 'generated-note.md',
|
|
2055
|
+
sourcePath: managedSourcePath
|
|
2056
|
+
});
|
|
2057
|
+
const after = new TaskforceCore({
|
|
2058
|
+
projectRoot,
|
|
2059
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2060
|
+
});
|
|
2061
|
+
try {
|
|
2062
|
+
const updated = after.getTask(task.id, 'workspace-1');
|
|
2063
|
+
expect(updated?.attachments).toHaveLength(1);
|
|
2064
|
+
}
|
|
2065
|
+
finally {
|
|
2066
|
+
after.close();
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
finally {
|
|
2070
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
it('rejects local sourcePath saves from arbitrary paths outside approved sources', async () => {
|
|
2074
|
+
const projectRoot = createProjectRoot();
|
|
2075
|
+
const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-external-'));
|
|
2076
|
+
try {
|
|
2077
|
+
const core = new TaskforceCore({
|
|
2078
|
+
projectRoot,
|
|
2079
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2080
|
+
});
|
|
2081
|
+
const task = core.createTask({ title: 'External source path reject test' }, { workspaceId: 'workspace-1' });
|
|
2082
|
+
core.close();
|
|
2083
|
+
const externalSourcePath = path.join(externalRoot, 'outside.md');
|
|
2084
|
+
fs.writeFileSync(externalSourcePath, '# Outside project and taskforce roots', 'utf8');
|
|
2085
|
+
const result = await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2086
|
+
id: task.referenceLabel,
|
|
2087
|
+
filename: 'outside.md',
|
|
2088
|
+
sourcePath: externalSourcePath
|
|
2089
|
+
});
|
|
2090
|
+
expect(result.content?.[0]?.text).toContain('sourcePath must be within project root or a Taskforce-managed attachment source');
|
|
2091
|
+
}
|
|
2092
|
+
finally {
|
|
2093
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2094
|
+
fs.rmSync(externalRoot, { recursive: true, force: true });
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
it('lists task attachments by internal id and task reference', async () => {
|
|
2098
|
+
const projectRoot = createProjectRoot();
|
|
2099
|
+
try {
|
|
2100
|
+
const core = new TaskforceCore({
|
|
2101
|
+
projectRoot,
|
|
2102
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2103
|
+
});
|
|
2104
|
+
const task = core.createTask({ title: 'List attachments test' }, { workspaceId: 'workspace-1' });
|
|
2105
|
+
const externalTask = core.createTask({ title: 'External attachment list test' }, { workspaceId: 'workspace-1' });
|
|
2106
|
+
core.close();
|
|
2107
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2108
|
+
id: task.id,
|
|
2109
|
+
filename: 'implementation-plan.txt',
|
|
2110
|
+
content: 'hello attachment'
|
|
2111
|
+
});
|
|
2112
|
+
await callTool(projectRoot, 'local', 'link_task_context', {
|
|
2113
|
+
id: externalTask.id,
|
|
2114
|
+
path: 'https://example.com/spec',
|
|
2115
|
+
caption: 'Spec'
|
|
2116
|
+
});
|
|
2117
|
+
const byId = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
2118
|
+
const byReference = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2119
|
+
const external = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: externalTask.id }));
|
|
2120
|
+
expect(byId.taskId).toBe(task.id);
|
|
2121
|
+
expect(byId.referenceLabel).toBe(task.referenceLabel);
|
|
2122
|
+
expect(byId.attachments).toHaveLength(1);
|
|
2123
|
+
expect(byId.attachments[0].mimeType).toBe('text/plain');
|
|
2124
|
+
expect(byId.attachments[0].readableAsText).toBe(true);
|
|
2125
|
+
expect(byId.attachments[0].attachmentId).toBe(byReference.attachments[0].attachmentId);
|
|
2126
|
+
expect(external.attachments).toHaveLength(1);
|
|
2127
|
+
expect(external.attachments[0].source).toBe('external');
|
|
2128
|
+
expect(external.attachments[0].readableAsText).toBe(false);
|
|
2129
|
+
expect(external.attachments[0].caption).toBe('Spec');
|
|
2130
|
+
}
|
|
2131
|
+
finally {
|
|
2132
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
it('accepts task references for write and batch update tools', async () => {
|
|
2136
|
+
const projectRoot = createProjectRoot();
|
|
2137
|
+
try {
|
|
2138
|
+
const core = new TaskforceCore({
|
|
2139
|
+
projectRoot,
|
|
2140
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2141
|
+
});
|
|
2142
|
+
const first = core.createTask({ title: 'First reference write test' }, { workspaceId: 'workspace-1' });
|
|
2143
|
+
const second = core.createTask({ title: 'Second reference write test' }, { workspaceId: 'workspace-1' });
|
|
2144
|
+
core.close();
|
|
2145
|
+
await callTool(projectRoot, 'local', 'schedule_task', {
|
|
2146
|
+
id: first.referenceLabel,
|
|
2147
|
+
scheduledDate: '2026-03-24'
|
|
2148
|
+
});
|
|
2149
|
+
await callTool(projectRoot, 'local', 'bulk_assign_tasks', {
|
|
2150
|
+
ids: [first.referenceLabel, second.referenceLabel],
|
|
2151
|
+
assignee: 'agent'
|
|
2152
|
+
});
|
|
2153
|
+
const after = new TaskforceCore({
|
|
2154
|
+
projectRoot,
|
|
2155
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2156
|
+
});
|
|
2157
|
+
try {
|
|
2158
|
+
const firstTask = after.getTask(first.id, 'workspace-1');
|
|
2159
|
+
const secondTask = after.getTask(second.id, 'workspace-1');
|
|
2160
|
+
expect(firstTask?.scheduledDate).toBe('2026-03-24');
|
|
2161
|
+
expect(firstTask?.assignee).toBe('unassigned');
|
|
2162
|
+
expect(secondTask?.assignee).toBe('unassigned');
|
|
2163
|
+
}
|
|
2164
|
+
finally {
|
|
2165
|
+
after.close();
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
finally {
|
|
2169
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
it('maps legacy agent assignee writes to unassigned even when a profile is resolved', async () => {
|
|
2173
|
+
const projectRoot = createProjectRoot();
|
|
2174
|
+
try {
|
|
2175
|
+
const runtime = createTaskforceMcpServer({
|
|
2176
|
+
projectRoot,
|
|
2177
|
+
runtimeMode: 'local',
|
|
2178
|
+
logStartup: false
|
|
2179
|
+
});
|
|
2180
|
+
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
2181
|
+
if (!handler) {
|
|
2182
|
+
throw new Error('tools/call handler not registered');
|
|
2183
|
+
}
|
|
2184
|
+
const resolved = parseToolJson(await handler({
|
|
2185
|
+
jsonrpc: '2.0',
|
|
2186
|
+
id: 'test-tools-call-resolve-profile',
|
|
2187
|
+
method: 'tools/call',
|
|
2188
|
+
params: {
|
|
2189
|
+
name: 'resolve_profile',
|
|
2190
|
+
arguments: {
|
|
2191
|
+
name: 'Codex',
|
|
2192
|
+
icon: 'Bot',
|
|
2193
|
+
color: '#ef6c00'
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}, {}));
|
|
2197
|
+
const created = parseToolJson(await handler({
|
|
2198
|
+
jsonrpc: '2.0',
|
|
2199
|
+
id: 'test-tools-call-create-task',
|
|
2200
|
+
method: 'tools/call',
|
|
2201
|
+
params: {
|
|
2202
|
+
name: 'create_task',
|
|
2203
|
+
arguments: {
|
|
2204
|
+
title: 'Profile-backed assignment',
|
|
2205
|
+
assignee: 'agent'
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}, {}));
|
|
2209
|
+
expect(created.assignee).toBe('unassigned');
|
|
2210
|
+
expect(created.createdBy).toBe(resolved.profile.id);
|
|
2211
|
+
}
|
|
2212
|
+
finally {
|
|
2213
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
it('logs profile-backed MCP task creation with the resolved AI profile instead of generic agent', async () => {
|
|
2217
|
+
const projectRoot = createProjectRoot();
|
|
2218
|
+
try {
|
|
2219
|
+
const runtime = createTaskforceMcpServer({
|
|
2220
|
+
projectRoot,
|
|
2221
|
+
runtimeMode: 'local',
|
|
2222
|
+
logStartup: false
|
|
2223
|
+
});
|
|
2224
|
+
const handler = runtime.server._requestHandlers.get('tools/call');
|
|
2225
|
+
if (!handler) {
|
|
2226
|
+
throw new Error('tools/call handler not registered');
|
|
2227
|
+
}
|
|
2228
|
+
const resolved = parseToolJson(await handler({
|
|
2229
|
+
jsonrpc: '2.0',
|
|
2230
|
+
id: 'test-tools-call-resolve-profile-for-created-by',
|
|
2231
|
+
method: 'tools/call',
|
|
2232
|
+
params: {
|
|
2233
|
+
name: 'resolve_profile',
|
|
2234
|
+
arguments: {
|
|
2235
|
+
name: 'Codex',
|
|
2236
|
+
icon: 'Bot',
|
|
2237
|
+
color: '#ef6c00'
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}, {}));
|
|
2241
|
+
const created = parseToolJson(await handler({
|
|
2242
|
+
jsonrpc: '2.0',
|
|
2243
|
+
id: 'test-tools-call-create-task-created-by',
|
|
2244
|
+
method: 'tools/call',
|
|
2245
|
+
params: {
|
|
2246
|
+
name: 'create_task',
|
|
2247
|
+
arguments: {
|
|
2248
|
+
title: 'Profile-backed createdBy'
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}, {}));
|
|
2252
|
+
expect(created.createdBy).toBe(resolved.profile.id);
|
|
2253
|
+
expect(created.createdByActor).toEqual(expect.objectContaining({
|
|
2254
|
+
id: resolved.profile.id,
|
|
2255
|
+
label: 'Codex',
|
|
2256
|
+
username: resolved.profile.username
|
|
2257
|
+
}));
|
|
2258
|
+
expect(created.taskEvents).toEqual(expect.arrayContaining([
|
|
2259
|
+
expect.objectContaining({
|
|
2260
|
+
action: 'task-created',
|
|
2261
|
+
actor: resolved.profile.id,
|
|
2262
|
+
actorType: 'ai',
|
|
2263
|
+
actorProfile: expect.objectContaining({
|
|
2264
|
+
id: resolved.profile.id,
|
|
2265
|
+
label: 'Codex',
|
|
2266
|
+
username: resolved.profile.username
|
|
2267
|
+
})
|
|
2268
|
+
})
|
|
2269
|
+
]));
|
|
2270
|
+
}
|
|
2271
|
+
finally {
|
|
2272
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
it('returns empty attachments for tasks without context files', async () => {
|
|
2276
|
+
const projectRoot = createProjectRoot();
|
|
2277
|
+
try {
|
|
2278
|
+
const core = new TaskforceCore({
|
|
2279
|
+
projectRoot,
|
|
2280
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2281
|
+
});
|
|
2282
|
+
const task = core.createTask({ title: 'No attachments test' }, { workspaceId: 'workspace-1' });
|
|
2283
|
+
core.close();
|
|
2284
|
+
const payload = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
2285
|
+
expect(payload.attachments).toEqual([]);
|
|
2286
|
+
}
|
|
2287
|
+
finally {
|
|
2288
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
it('reads markdown task attachments as inline text in local and cloud runtimes', async () => {
|
|
2292
|
+
const projectRoot = createProjectRoot();
|
|
2293
|
+
try {
|
|
2294
|
+
const core = new TaskforceCore({
|
|
2295
|
+
projectRoot,
|
|
2296
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2297
|
+
});
|
|
2298
|
+
const task = core.createTask({ title: 'Read attachment test' }, { workspaceId: 'workspace-1' });
|
|
2299
|
+
core.close();
|
|
2300
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2301
|
+
id: task.id,
|
|
2302
|
+
filename: 'notes.md',
|
|
2303
|
+
content: '# Heading\nBody'
|
|
2304
|
+
});
|
|
2305
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
2306
|
+
const attachmentId = listed.attachments[0]?.attachmentId;
|
|
2307
|
+
expect(attachmentId).toBeTruthy();
|
|
2308
|
+
const localRead = parseToolJson(await callTool(projectRoot, 'local', 'read_task_attachment', { taskId: task.id, attachmentId }));
|
|
2309
|
+
const cloudRead = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', { taskId: task.referenceLabel, attachmentId }));
|
|
2310
|
+
expect(localRead.format).toBe('text');
|
|
2311
|
+
expect(localRead.truncated).toBe(false);
|
|
2312
|
+
expect(localRead.content).toContain('# Heading');
|
|
2313
|
+
expect(cloudRead.content).toContain('Body');
|
|
2314
|
+
}
|
|
2315
|
+
finally {
|
|
2316
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
it('returns canonical document workflow hints for markdown task attachments', async () => {
|
|
2320
|
+
const projectRoot = createProjectRoot();
|
|
2321
|
+
try {
|
|
2322
|
+
const core = new TaskforceCore({
|
|
2323
|
+
projectRoot,
|
|
2324
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2325
|
+
});
|
|
2326
|
+
const task = core.createTask({ title: 'Canonical document attachment hints test' }, { workspaceId: 'workspace-1' });
|
|
2327
|
+
core.close();
|
|
2328
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2329
|
+
id: task.referenceLabel,
|
|
2330
|
+
filename: 'design-notes.md',
|
|
2331
|
+
content: '# Design notes\nBody'
|
|
2332
|
+
});
|
|
2333
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2334
|
+
const attachment = listed.attachments[0];
|
|
2335
|
+
expect(attachment?.kind).toBe('document');
|
|
2336
|
+
expect(attachment?.documentId).toBeTruthy();
|
|
2337
|
+
expect(attachment?.referenceLabel).toMatch(/^D-\d+$/);
|
|
2338
|
+
expect(attachment?.preferredNextTool).toBe('get_document');
|
|
2339
|
+
expect(attachment?.preferredWorkflow).toBe('canonical_document');
|
|
2340
|
+
expect(attachment?.suggestedArgs?.documentId).toBe(attachment?.referenceLabel);
|
|
2341
|
+
expect(attachment?.guidance).toContain('canonical document');
|
|
2342
|
+
const metadata = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2343
|
+
taskId: task.referenceLabel,
|
|
2344
|
+
attachmentId: attachment?.attachmentId,
|
|
2345
|
+
format: 'metadata'
|
|
2346
|
+
}));
|
|
2347
|
+
expect(metadata.format).toBe('metadata');
|
|
2348
|
+
expect(metadata.attachment.documentId).toBe(attachment?.documentId);
|
|
2349
|
+
expect(metadata.attachment.preferredNextTool).toBe('get_document');
|
|
2350
|
+
expect(metadata.attachment.preferredWorkflow).toBe('canonical_document');
|
|
2351
|
+
expect(metadata.attachment.suggestedArgs?.documentId).toBe(attachment?.referenceLabel);
|
|
2352
|
+
}
|
|
2353
|
+
finally {
|
|
2354
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
it('does not assign document references or canonical document hints to txt task attachments', async () => {
|
|
2358
|
+
const projectRoot = createProjectRoot();
|
|
2359
|
+
try {
|
|
2360
|
+
const core = new TaskforceCore({
|
|
2361
|
+
projectRoot,
|
|
2362
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2363
|
+
});
|
|
2364
|
+
const task = core.createTask({ title: 'Plain text attachment invariants' }, { workspaceId: 'workspace-1' });
|
|
2365
|
+
core.close();
|
|
2366
|
+
await callTool(projectRoot, 'cloud', 'save_task_attachment', {
|
|
2367
|
+
id: task.referenceLabel,
|
|
2368
|
+
filename: 'design-notes.txt',
|
|
2369
|
+
content: 'plain text note'
|
|
2370
|
+
});
|
|
2371
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.referenceLabel }));
|
|
2372
|
+
const attachment = listed.attachments[0];
|
|
2373
|
+
expect(attachment?.kind).toBe('file');
|
|
2374
|
+
expect(attachment?.referenceLabel || null).toBeNull();
|
|
2375
|
+
expect(attachment?.preferredNextTool).toBeUndefined();
|
|
2376
|
+
expect(attachment?.preferredWorkflow).toBeUndefined();
|
|
2377
|
+
expect(attachment?.suggestedArgs).toBeUndefined();
|
|
2378
|
+
}
|
|
2379
|
+
finally {
|
|
2380
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
it('returns download metadata for binary attachments instead of inline text', async () => {
|
|
2384
|
+
const projectRoot = createProjectRoot();
|
|
2385
|
+
try {
|
|
2386
|
+
const core = new TaskforceCore({
|
|
2387
|
+
projectRoot,
|
|
2388
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2389
|
+
});
|
|
2390
|
+
const task = core.createTask({ title: 'Binary attachment test' }, { workspaceId: 'workspace-1' });
|
|
2391
|
+
core.close();
|
|
2392
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2393
|
+
id: task.id,
|
|
2394
|
+
filename: 'proof.png',
|
|
2395
|
+
content: 'fake-image-bits'
|
|
2396
|
+
});
|
|
2397
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
2398
|
+
const attachmentId = listed.attachments[0]?.attachmentId;
|
|
2399
|
+
const response = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', { taskId: task.id, attachmentId }));
|
|
2400
|
+
expect(listed.attachments[0]?.mimeType).toBe('image/png');
|
|
2401
|
+
expect(listed.attachments[0]?.readableAsText).toBe(false);
|
|
2402
|
+
expect(response.format).toBe('download');
|
|
2403
|
+
expect(response.reason).toBe('binary_file');
|
|
2404
|
+
expect(response.downloadUrl).toContain('download=1');
|
|
2405
|
+
}
|
|
2406
|
+
finally {
|
|
2407
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
it('rejects inline reads for quarantined and pending attachments', async () => {
|
|
2411
|
+
const projectRoot = createProjectRoot();
|
|
2412
|
+
try {
|
|
2413
|
+
const core = new TaskforceCore({
|
|
2414
|
+
projectRoot,
|
|
2415
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2416
|
+
});
|
|
2417
|
+
const pendingTask = core.createTask({ title: 'Pending attachment test' }, { workspaceId: 'workspace-1' });
|
|
2418
|
+
const infectedTask = core.createTask({ title: 'Infected attachment test' }, { workspaceId: 'workspace-1' });
|
|
2419
|
+
core.close();
|
|
2420
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2421
|
+
id: pendingTask.id,
|
|
2422
|
+
filename: 'pending.md',
|
|
2423
|
+
content: '# Pending'
|
|
2424
|
+
});
|
|
2425
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2426
|
+
id: infectedTask.id,
|
|
2427
|
+
filename: 'infected.md',
|
|
2428
|
+
content: '# Infected'
|
|
2429
|
+
});
|
|
2430
|
+
const dbCore = new TaskforceCore({
|
|
2431
|
+
projectRoot,
|
|
2432
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2433
|
+
});
|
|
2434
|
+
try {
|
|
2435
|
+
const store = new WorkspaceAssetStore(dbCore.getDatabaseAdapter(), dbCore.getTenantId());
|
|
2436
|
+
const pendingAttachment = dbCore.getTask(pendingTask.id, 'workspace-1')?.attachments?.[0];
|
|
2437
|
+
const infectedAttachment = dbCore.getTask(infectedTask.id, 'workspace-1')?.attachments?.[0];
|
|
2438
|
+
const pendingAsset = store.get(String(pendingAttachment?.assetId || ''), 'workspace-1');
|
|
2439
|
+
const infectedAsset = store.get(String(infectedAttachment?.assetId || ''), 'workspace-1');
|
|
2440
|
+
expect(pendingAsset).toBeTruthy();
|
|
2441
|
+
expect(infectedAsset).toBeTruthy();
|
|
2442
|
+
store.upsertAsset({
|
|
2443
|
+
...pendingAsset,
|
|
2444
|
+
scanStatus: 'pending'
|
|
2445
|
+
});
|
|
2446
|
+
store.upsertAsset({
|
|
2447
|
+
...infectedAsset,
|
|
2448
|
+
scanStatus: 'infected'
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
finally {
|
|
2452
|
+
dbCore.close();
|
|
2453
|
+
}
|
|
2454
|
+
const pendingList = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: pendingTask.id }));
|
|
2455
|
+
const infectedList = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: infectedTask.id }));
|
|
2456
|
+
const pendingRead = await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2457
|
+
taskId: pendingTask.id,
|
|
2458
|
+
attachmentId: pendingList.attachments[0]?.attachmentId
|
|
2459
|
+
});
|
|
2460
|
+
const infectedRead = await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2461
|
+
taskId: infectedTask.id,
|
|
2462
|
+
attachmentId: infectedList.attachments[0]?.attachmentId
|
|
2463
|
+
});
|
|
2464
|
+
expect(pendingRead.content?.[0]?.text).toContain('Attachment scan pending');
|
|
2465
|
+
expect(infectedRead.content?.[0]?.text).toContain('Attachment is quarantined');
|
|
2466
|
+
}
|
|
2467
|
+
finally {
|
|
2468
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2469
|
+
}
|
|
2470
|
+
});
|
|
2471
|
+
it('returns a readable diagnostic when an r2 attachment cannot be read in this runtime', async () => {
|
|
2472
|
+
const projectRoot = createProjectRoot();
|
|
2473
|
+
try {
|
|
2474
|
+
const core = new TaskforceCore({
|
|
2475
|
+
projectRoot,
|
|
2476
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2477
|
+
});
|
|
2478
|
+
const task = core.createTask({ title: 'R2 diagnostic test' }, { workspaceId: 'workspace-1' });
|
|
2479
|
+
core.close();
|
|
2480
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2481
|
+
id: task.id,
|
|
2482
|
+
filename: 'diagnostic.txt',
|
|
2483
|
+
content: 'hello'
|
|
2484
|
+
});
|
|
2485
|
+
const dbCore = new TaskforceCore({
|
|
2486
|
+
projectRoot,
|
|
2487
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2488
|
+
});
|
|
2489
|
+
try {
|
|
2490
|
+
const store = new WorkspaceAssetStore(dbCore.getDatabaseAdapter(), dbCore.getTenantId());
|
|
2491
|
+
const attachment = dbCore.getTask(task.id, 'workspace-1')?.attachments?.[0];
|
|
2492
|
+
const asset = store.get(String(attachment?.assetId || ''), 'workspace-1');
|
|
2493
|
+
expect(asset).toBeTruthy();
|
|
2494
|
+
store.upsertAsset({
|
|
2495
|
+
...asset,
|
|
2496
|
+
storageProvider: 'r2'
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
finally {
|
|
2500
|
+
dbCore.close();
|
|
2501
|
+
}
|
|
2502
|
+
const listed = parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.id }));
|
|
2503
|
+
const result = await callTool(projectRoot, 'cloud', 'read_task_attachment', {
|
|
2504
|
+
taskId: task.id,
|
|
2505
|
+
attachmentId: listed.attachments[0]?.attachmentId
|
|
2506
|
+
});
|
|
2507
|
+
expect(result.content?.[0]?.text).toContain('Attachment read failed from R2 because object storage is not configured');
|
|
2508
|
+
expect(result.content?.[0]?.text).toContain('attachmentId=');
|
|
2509
|
+
expect(result.content?.[0]?.text).toContain('storageKey=');
|
|
2510
|
+
}
|
|
2511
|
+
finally {
|
|
2512
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2513
|
+
}
|
|
2514
|
+
});
|
|
2515
|
+
it('preserves attachment ordering in get_task_attachments', async () => {
|
|
2516
|
+
const projectRoot = createProjectRoot();
|
|
2517
|
+
try {
|
|
2518
|
+
const core = new TaskforceCore({
|
|
2519
|
+
projectRoot,
|
|
2520
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2521
|
+
});
|
|
2522
|
+
const task = core.createTask({ title: 'Attachment order test' }, { workspaceId: 'workspace-1' });
|
|
2523
|
+
core.close();
|
|
2524
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2525
|
+
id: task.id,
|
|
2526
|
+
filename: 'first.md',
|
|
2527
|
+
content: '# First',
|
|
2528
|
+
caption: 'First'
|
|
2529
|
+
});
|
|
2530
|
+
await callTool(projectRoot, 'local', 'save_task_attachment', {
|
|
2531
|
+
id: task.id,
|
|
2532
|
+
filename: 'second.md',
|
|
2533
|
+
content: '# Second',
|
|
2534
|
+
caption: 'Second'
|
|
2535
|
+
});
|
|
2536
|
+
const listed = parseToolJson(await callTool(projectRoot, 'local', 'get_task_attachments', { id: task.id }));
|
|
2537
|
+
expect(listed.attachments.map((attachment) => attachment.caption)).toEqual(['First', 'Second']);
|
|
2538
|
+
}
|
|
2539
|
+
finally {
|
|
2540
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2541
|
+
}
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
describe('Taskforce MCP annotated attachment tools', () => {
|
|
2545
|
+
let previousGateEnabled = false;
|
|
2546
|
+
beforeEach(() => {
|
|
2547
|
+
previousGateEnabled = APP_GATE_DEFINITIONS[ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY].enabled;
|
|
2548
|
+
APP_GATE_DEFINITIONS[ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY].enabled = true;
|
|
2549
|
+
});
|
|
2550
|
+
afterEach(() => {
|
|
2551
|
+
APP_GATE_DEFINITIONS[ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY].enabled = previousGateEnabled;
|
|
2552
|
+
});
|
|
2553
|
+
it('lists annotated attachment sessions and reads payloads', async () => {
|
|
2554
|
+
const projectRoot = createProjectRoot();
|
|
2555
|
+
try {
|
|
2556
|
+
const { task, session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2557
|
+
const sessionsResult = parseToolJson(await callTool(projectRoot, 'local', 'get_annotated_attachment_sessions', {
|
|
2558
|
+
taskId: task.referenceLabel || task.id,
|
|
2559
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2560
|
+
expect(sessionsResult.taskId).toBe(task.id);
|
|
2561
|
+
expect(sessionsResult.sessions).toHaveLength(1);
|
|
2562
|
+
expect(sessionsResult.sessions[0]).toMatchObject({
|
|
2563
|
+
id: session.id,
|
|
2564
|
+
title: 'Fixture session',
|
|
2565
|
+
});
|
|
2566
|
+
const imageResult = parseToolJson(await callTool(projectRoot, 'local', 'get_image', {
|
|
2567
|
+
imageId: 'I-1',
|
|
2568
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2569
|
+
expect(imageResult.image).toMatchObject({
|
|
2570
|
+
assetId: 'asset-annotated-1',
|
|
2571
|
+
referenceLabel: 'I-1',
|
|
2572
|
+
});
|
|
2573
|
+
expect(imageResult.linkedTasks[0]).toMatchObject({
|
|
2574
|
+
taskId: task.id,
|
|
2575
|
+
referenceLabel: task.referenceLabel,
|
|
2576
|
+
});
|
|
2577
|
+
expect(imageResult.linkedTasks[0]?.sessions[0]?.id).toBe(session.id);
|
|
2578
|
+
const imageSessionsResult = parseToolJson(await callTool(projectRoot, 'local', 'get_annotated_attachment_sessions', {
|
|
2579
|
+
imageId: 'I-1',
|
|
2580
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2581
|
+
expect(imageSessionsResult.image).toMatchObject({
|
|
2582
|
+
assetId: 'asset-annotated-1',
|
|
2583
|
+
referenceLabel: 'I-1',
|
|
2584
|
+
});
|
|
2585
|
+
expect(imageSessionsResult.tasks[0]?.taskId).toBe(task.id);
|
|
2586
|
+
expect(imageSessionsResult.tasks[0]?.sessions[0]?.id).toBe(session.id);
|
|
2587
|
+
const payloadResult = parseToolJson(await callTool(projectRoot, 'local', 'read_annotated_attachment_payload', {
|
|
2588
|
+
sessionId: session.id,
|
|
2589
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2590
|
+
expect(payloadResult).toMatchObject({
|
|
2591
|
+
taskId: task.id,
|
|
2592
|
+
sessionId: session.id,
|
|
2593
|
+
title: 'Fixture session',
|
|
2594
|
+
image: {
|
|
2595
|
+
assetId: 'asset-annotated-1',
|
|
2596
|
+
referenceLabel: 'I-1',
|
|
2597
|
+
},
|
|
2598
|
+
});
|
|
2599
|
+
expect(payloadResult.annotations[0].id).toBe('annotation-1');
|
|
2600
|
+
expect(payloadResult.annotations[0].kind).toBe('pin');
|
|
2601
|
+
expect(payloadResult.annotations[0].cropHint.x).toBeCloseTo(0.16, 10);
|
|
2602
|
+
expect(payloadResult.annotations[0].cropHint.y).toBeCloseTo(0.31, 10);
|
|
2603
|
+
expect(payloadResult.annotations[0].cropHint.width).toBeCloseTo(0.18, 10);
|
|
2604
|
+
expect(payloadResult.annotations[0].cropHint.height).toBeCloseTo(0.18, 10);
|
|
2605
|
+
}
|
|
2606
|
+
finally {
|
|
2607
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
it('includes unattached image note sessions in image-centric MCP reads', async () => {
|
|
2611
|
+
const projectRoot = createProjectRoot();
|
|
2612
|
+
try {
|
|
2613
|
+
const { task, session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2614
|
+
const core = new TaskforceCore({
|
|
2615
|
+
projectRoot,
|
|
2616
|
+
storagePath: path.join(projectRoot, '.taskforce'),
|
|
2617
|
+
});
|
|
2618
|
+
try {
|
|
2619
|
+
core.createAnnotatedAttachmentSession({
|
|
2620
|
+
workspaceId: 'workspace-1',
|
|
2621
|
+
taskId: null,
|
|
2622
|
+
baseImageAssetId: 'asset-annotated-1',
|
|
2623
|
+
title: 'Unattached image review',
|
|
2624
|
+
globalInstruction: 'Keep this separate from task notes.',
|
|
2625
|
+
actorId: 'owner-1',
|
|
2626
|
+
annotations: [
|
|
2627
|
+
{
|
|
2628
|
+
id: 'annotation-unattached-1',
|
|
2629
|
+
kind: 'pin',
|
|
2630
|
+
order: 0,
|
|
2631
|
+
markerType: 'review',
|
|
2632
|
+
instruction: 'Review the unattached image state.',
|
|
2633
|
+
x: 0.5,
|
|
2634
|
+
y: 0.5,
|
|
2635
|
+
},
|
|
2636
|
+
],
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
finally {
|
|
2640
|
+
core.close();
|
|
2641
|
+
}
|
|
2642
|
+
const imageResult = parseToolJson(await callTool(projectRoot, 'local', 'get_image', {
|
|
2643
|
+
imageId: 'I-1',
|
|
2644
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2645
|
+
expect(imageResult.image).toMatchObject({
|
|
2646
|
+
assetId: 'asset-annotated-1',
|
|
2647
|
+
referenceLabel: 'I-1',
|
|
2648
|
+
});
|
|
2649
|
+
expect(imageResult.linkedTasks[0]?.taskId).toBe(task.id);
|
|
2650
|
+
expect(imageResult.linkedTasks[0]?.sessions[0]?.id).toBe(session.id);
|
|
2651
|
+
expect(imageResult.unattachedSessions).toHaveLength(1);
|
|
2652
|
+
expect(imageResult.unattachedSessions[0]).toMatchObject({
|
|
2653
|
+
title: 'Unattached image review',
|
|
2654
|
+
});
|
|
2655
|
+
const imageSessionsResult = parseToolJson(await callTool(projectRoot, 'local', 'get_annotated_attachment_sessions', {
|
|
2656
|
+
imageId: 'I-1',
|
|
2657
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2658
|
+
expect(imageSessionsResult.image).toMatchObject({
|
|
2659
|
+
assetId: 'asset-annotated-1',
|
|
2660
|
+
referenceLabel: 'I-1',
|
|
2661
|
+
});
|
|
2662
|
+
expect(imageSessionsResult.tasks[0]?.taskId).toBe(task.id);
|
|
2663
|
+
expect(imageSessionsResult.tasks[0]?.sessions[0]?.id).toBe(session.id);
|
|
2664
|
+
expect(imageSessionsResult.unattachedSessions).toHaveLength(1);
|
|
2665
|
+
expect(imageSessionsResult.unattachedSessions[0]).toMatchObject({
|
|
2666
|
+
title: 'Unattached image review',
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
finally {
|
|
2670
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
it('searches canonical images by reference label and filename fragment', async () => {
|
|
2674
|
+
const projectRoot = createProjectRoot();
|
|
2675
|
+
try {
|
|
2676
|
+
seedAnnotatedAttachmentFixture(projectRoot);
|
|
2677
|
+
const core = new TaskforceCore({
|
|
2678
|
+
projectRoot,
|
|
2679
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2680
|
+
});
|
|
2681
|
+
try {
|
|
2682
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
2683
|
+
assetStore.upsertAsset({
|
|
2684
|
+
assetId: 'asset-annotated-2',
|
|
2685
|
+
workspaceId: 'workspace-1',
|
|
2686
|
+
kind: 'image',
|
|
2687
|
+
logicalName: 'Roadmap screenshot',
|
|
2688
|
+
originalFilename: 'roadmap-screenshot.png',
|
|
2689
|
+
mimeType: 'image/png',
|
|
2690
|
+
storageProvider: 'local',
|
|
2691
|
+
storageKey: 'workspaces/workspace-1/assets/images/asset-annotated-2-roadmap-screenshot.png',
|
|
2692
|
+
contentSha256: 'sha-annotated-2',
|
|
2693
|
+
sizeBytes: 768,
|
|
2694
|
+
referenceNumber: 2,
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
finally {
|
|
2698
|
+
core.close();
|
|
2699
|
+
}
|
|
2700
|
+
const byReference = parseToolJson(await callTool(projectRoot, 'local', 'search_images', {
|
|
2701
|
+
query: 'I-1',
|
|
2702
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2703
|
+
expect(byReference.results[0]).toMatchObject({
|
|
2704
|
+
image: {
|
|
2705
|
+
assetId: 'asset-annotated-1',
|
|
2706
|
+
referenceLabel: 'I-1',
|
|
2707
|
+
},
|
|
2708
|
+
nextTool: 'get_image',
|
|
2709
|
+
suggestedArgs: { imageId: 'I-1' },
|
|
2710
|
+
});
|
|
2711
|
+
expect(byReference.results[0]?.matchedOn).toContain('referenceLabel');
|
|
2712
|
+
expect(byReference.guidance).toContain('call get_image');
|
|
2713
|
+
const byFilename = parseToolJson(await callTool(projectRoot, 'local', 'search_images', {
|
|
2714
|
+
query: 'roadmap-screenshot',
|
|
2715
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2716
|
+
expect(byFilename.results[0]).toMatchObject({
|
|
2717
|
+
image: {
|
|
2718
|
+
assetId: 'asset-annotated-2',
|
|
2719
|
+
referenceLabel: 'I-2',
|
|
2720
|
+
},
|
|
2721
|
+
suggestedArgs: { imageId: 'I-2' },
|
|
2722
|
+
});
|
|
2723
|
+
expect(byFilename.results[0]?.matchedOn).toContain('originalFilename');
|
|
2724
|
+
}
|
|
2725
|
+
finally {
|
|
2726
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
it('nudges agents toward search_images when exact image lookup fails', async () => {
|
|
2730
|
+
const projectRoot = createProjectRoot();
|
|
2731
|
+
try {
|
|
2732
|
+
seedAnnotatedAttachmentFixture(projectRoot);
|
|
2733
|
+
const result = await callTool(projectRoot, 'local', 'get_image', {
|
|
2734
|
+
imageId: 'roadmap image',
|
|
2735
|
+
}, { workspaceId: 'workspace-1' });
|
|
2736
|
+
expect(result.isError).toBe(true);
|
|
2737
|
+
expect(result.content?.[0]?.text).toContain('call search_images first');
|
|
2738
|
+
}
|
|
2739
|
+
finally {
|
|
2740
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2741
|
+
}
|
|
2742
|
+
});
|
|
2743
|
+
it('creates image-note sessions and mutates markers through MCP', async () => {
|
|
2744
|
+
const projectRoot = createProjectRoot();
|
|
2745
|
+
try {
|
|
2746
|
+
const { task } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2747
|
+
const createdSession = parseToolJson(await callTool(projectRoot, 'local', 'create_annotated_attachment_session', {
|
|
2748
|
+
imageId: 'asset-annotated-1',
|
|
2749
|
+
taskId: task.id,
|
|
2750
|
+
title: 'AI walkthrough',
|
|
2751
|
+
sessionInstruction: 'Review the flow in order.',
|
|
2752
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2753
|
+
expect(createdSession.image.assetId).toBe('asset-annotated-1');
|
|
2754
|
+
expect(createdSession.session.title).toBe('AI walkthrough');
|
|
2755
|
+
expect(createdSession.session.globalInstruction).toBe('Review the flow in order.');
|
|
2756
|
+
expect(createdSession.session.annotations).toHaveLength(0);
|
|
2757
|
+
const addedMarker = parseToolJson(await callTool(projectRoot, 'local', 'add_annotated_attachment_marker', {
|
|
2758
|
+
sessionId: createdSession.session.id,
|
|
2759
|
+
kind: 'box',
|
|
2760
|
+
instruction: 'Focus attention on this panel.',
|
|
2761
|
+
instructionType: 'change',
|
|
2762
|
+
x: 0.2,
|
|
2763
|
+
y: 0.3,
|
|
2764
|
+
width: 0.25,
|
|
2765
|
+
height: 0.15,
|
|
2766
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2767
|
+
expect(addedMarker.marker.kind).toBe('box');
|
|
2768
|
+
expect(addedMarker.marker.markerType).toBe('change');
|
|
2769
|
+
expect(addedMarker.session.annotations).toHaveLength(1);
|
|
2770
|
+
const updatedMarker = parseToolJson(await callTool(projectRoot, 'local', 'update_annotated_attachment_marker', {
|
|
2771
|
+
sessionId: createdSession.session.id,
|
|
2772
|
+
markerId: addedMarker.marker.id,
|
|
2773
|
+
instruction: 'Focus attention on this primary panel.',
|
|
2774
|
+
instructionType: 'review',
|
|
2775
|
+
width: 0.3,
|
|
2776
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2777
|
+
expect(updatedMarker.marker.instruction).toBe('Focus attention on this primary panel.');
|
|
2778
|
+
expect(updatedMarker.marker.markerType).toBe('review');
|
|
2779
|
+
expect(updatedMarker.marker.width).toBeCloseTo(0.3, 10);
|
|
2780
|
+
const updatedSession = parseToolJson(await callTool(projectRoot, 'local', 'update_annotated_attachment_session', {
|
|
2781
|
+
sessionId: createdSession.session.id,
|
|
2782
|
+
title: 'AI walkthrough v2',
|
|
2783
|
+
sessionInstruction: 'Answer each marker in order.',
|
|
2784
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2785
|
+
expect(updatedSession.session.title).toBe('AI walkthrough v2');
|
|
2786
|
+
expect(updatedSession.session.globalInstruction).toBe('Answer each marker in order.');
|
|
2787
|
+
const deletedMarker = parseToolJson(await callTool(projectRoot, 'local', 'delete_annotated_attachment_marker', {
|
|
2788
|
+
sessionId: createdSession.session.id,
|
|
2789
|
+
markerId: addedMarker.marker.id,
|
|
2790
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2791
|
+
expect(deletedMarker.deletedMarkerId).toBe(addedMarker.marker.id);
|
|
2792
|
+
expect(deletedMarker.session.annotations).toHaveLength(0);
|
|
2793
|
+
const verifyCore = new TaskforceCore({
|
|
2794
|
+
projectRoot,
|
|
2795
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2796
|
+
});
|
|
2797
|
+
try {
|
|
2798
|
+
const persisted = verifyCore.getAnnotatedAttachmentSession({
|
|
2799
|
+
workspaceId: 'workspace-1',
|
|
2800
|
+
sessionId: createdSession.session.id,
|
|
2801
|
+
});
|
|
2802
|
+
expect(persisted?.title).toBe('AI walkthrough v2');
|
|
2803
|
+
expect(persisted?.globalInstruction).toBe('Answer each marker in order.');
|
|
2804
|
+
expect(persisted?.annotations).toHaveLength(0);
|
|
2805
|
+
}
|
|
2806
|
+
finally {
|
|
2807
|
+
verifyCore.close();
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
finally {
|
|
2811
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
it('bulk adds and reorders image-note markers through MCP', async () => {
|
|
2815
|
+
const projectRoot = createProjectRoot();
|
|
2816
|
+
try {
|
|
2817
|
+
const { task, session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2818
|
+
const bulkAdded = parseToolJson(await callTool(projectRoot, 'local', 'bulk_add_annotated_attachment_markers', {
|
|
2819
|
+
sessionId: session.id,
|
|
2820
|
+
markers: [
|
|
2821
|
+
{
|
|
2822
|
+
kind: 'pin',
|
|
2823
|
+
instruction: 'Start here.',
|
|
2824
|
+
instructionType: 'review',
|
|
2825
|
+
x: 0.1,
|
|
2826
|
+
y: 0.15,
|
|
2827
|
+
},
|
|
2828
|
+
{
|
|
2829
|
+
kind: 'arrow',
|
|
2830
|
+
instruction: 'Move toward this action.',
|
|
2831
|
+
instructionType: 'change',
|
|
2832
|
+
x: 0.2,
|
|
2833
|
+
y: 0.25,
|
|
2834
|
+
x2: 0.4,
|
|
2835
|
+
y2: 0.45,
|
|
2836
|
+
},
|
|
2837
|
+
],
|
|
2838
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2839
|
+
expect(bulkAdded.markers).toHaveLength(2);
|
|
2840
|
+
expect(bulkAdded.session.annotations).toHaveLength(3);
|
|
2841
|
+
expect(bulkAdded.session.annotations.map((annotation) => annotation.order)).toEqual([0, 1, 2]);
|
|
2842
|
+
const movedMarkerId = bulkAdded.markers[1]?.id;
|
|
2843
|
+
const reordered = parseToolJson(await callTool(projectRoot, 'local', 'reorder_annotated_attachment_marker', {
|
|
2844
|
+
sessionId: session.id,
|
|
2845
|
+
markerId: movedMarkerId,
|
|
2846
|
+
toIndex: 0,
|
|
2847
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2848
|
+
expect(reordered.session.annotations.map((annotation) => annotation.id)).toEqual([
|
|
2849
|
+
movedMarkerId,
|
|
2850
|
+
'annotation-1',
|
|
2851
|
+
bulkAdded.markers[0]?.id,
|
|
2852
|
+
]);
|
|
2853
|
+
expect(reordered.session.annotations.map((annotation) => annotation.order)).toEqual([0, 1, 2]);
|
|
2854
|
+
}
|
|
2855
|
+
finally {
|
|
2856
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
it('deletes image-note sessions through MCP', async () => {
|
|
2860
|
+
const projectRoot = createProjectRoot();
|
|
2861
|
+
try {
|
|
2862
|
+
const { session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2863
|
+
const deleted = parseToolJson(await callTool(projectRoot, 'local', 'delete_annotated_attachment_session', {
|
|
2864
|
+
sessionId: session.id,
|
|
2865
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2866
|
+
expect(deleted.deleted).toBe(true);
|
|
2867
|
+
expect(deleted.deletedSessionId).toBe(session.id);
|
|
2868
|
+
expect(deleted.image?.assetId).toBe('asset-annotated-1');
|
|
2869
|
+
const verifyCore = new TaskforceCore({
|
|
2870
|
+
projectRoot,
|
|
2871
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2872
|
+
});
|
|
2873
|
+
try {
|
|
2874
|
+
const persisted = verifyCore.getAnnotatedAttachmentSession({
|
|
2875
|
+
workspaceId: 'workspace-1',
|
|
2876
|
+
sessionId: session.id,
|
|
2877
|
+
});
|
|
2878
|
+
expect(persisted).toBeNull();
|
|
2879
|
+
}
|
|
2880
|
+
finally {
|
|
2881
|
+
verifyCore.close();
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
finally {
|
|
2885
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
it('reviews an image through the helper by creating or reusing a session', async () => {
|
|
2889
|
+
const projectRoot = createProjectRoot();
|
|
2890
|
+
try {
|
|
2891
|
+
const { task, session } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2892
|
+
const created = parseToolJson(await callTool(projectRoot, 'local', 'review_image', {
|
|
2893
|
+
imageId: 'asset-annotated-1',
|
|
2894
|
+
taskId: task.id,
|
|
2895
|
+
title: 'AI image review',
|
|
2896
|
+
sessionInstruction: 'Address each comment in order.',
|
|
2897
|
+
markers: [
|
|
2898
|
+
{
|
|
2899
|
+
kind: 'pin',
|
|
2900
|
+
instruction: 'Check this entry point.',
|
|
2901
|
+
x: 0.22,
|
|
2902
|
+
y: 0.33,
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
kind: 'arrow',
|
|
2906
|
+
instruction: 'Then move to this control.',
|
|
2907
|
+
instructionType: 'change',
|
|
2908
|
+
x: 0.3,
|
|
2909
|
+
y: 0.4,
|
|
2910
|
+
x2: 0.48,
|
|
2911
|
+
y2: 0.51,
|
|
2912
|
+
},
|
|
2913
|
+
],
|
|
2914
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2915
|
+
expect(created.session.title).toBe('AI image review');
|
|
2916
|
+
expect(created.session.globalInstruction).toBe('Address each comment in order.');
|
|
2917
|
+
expect(created.markers).toHaveLength(2);
|
|
2918
|
+
expect(created.session.annotations).toHaveLength(2);
|
|
2919
|
+
const reused = parseToolJson(await callTool(projectRoot, 'local', 'review_image', {
|
|
2920
|
+
imageId: 'asset-annotated-1',
|
|
2921
|
+
taskId: task.id,
|
|
2922
|
+
sessionId: session.id,
|
|
2923
|
+
sessionInstruction: 'Reply in sequence.',
|
|
2924
|
+
markers: [
|
|
2925
|
+
{
|
|
2926
|
+
kind: 'text-note',
|
|
2927
|
+
instruction: 'Summarize the overall issue here.',
|
|
2928
|
+
x: 0.55,
|
|
2929
|
+
y: 0.6,
|
|
2930
|
+
},
|
|
2931
|
+
],
|
|
2932
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2933
|
+
expect(reused.session.id).toBe(session.id);
|
|
2934
|
+
expect(reused.markers).toHaveLength(1);
|
|
2935
|
+
expect(reused.session.annotations).toHaveLength(2);
|
|
2936
|
+
const verifyCore = new TaskforceCore({
|
|
2937
|
+
projectRoot,
|
|
2938
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2939
|
+
});
|
|
2940
|
+
try {
|
|
2941
|
+
const reusedPersisted = verifyCore.getAnnotatedAttachmentSession({
|
|
2942
|
+
workspaceId: 'workspace-1',
|
|
2943
|
+
sessionId: session.id,
|
|
2944
|
+
});
|
|
2945
|
+
expect(reusedPersisted?.globalInstruction).toBe('Reply in sequence.');
|
|
2946
|
+
expect(reusedPersisted?.annotations).toHaveLength(2);
|
|
2947
|
+
}
|
|
2948
|
+
finally {
|
|
2949
|
+
verifyCore.close();
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
finally {
|
|
2953
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
it('rejects annotated attachment MCP reads when the feature gate is disabled', async () => {
|
|
2957
|
+
APP_GATE_DEFINITIONS[ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY].enabled = false;
|
|
2958
|
+
const projectRoot = createProjectRoot();
|
|
2959
|
+
try {
|
|
2960
|
+
const { task } = seedAnnotatedAttachmentFixture(projectRoot);
|
|
2961
|
+
const result = await callTool(projectRoot, 'local', 'get_annotated_attachment_sessions', {
|
|
2962
|
+
taskId: task.id,
|
|
2963
|
+
}, { workspaceId: 'workspace-1' });
|
|
2964
|
+
expect(result.isError).toBe(true);
|
|
2965
|
+
expect(result.content?.[0]?.text).toContain('Annotated attachments are unavailable for this workspace.');
|
|
2966
|
+
}
|
|
2967
|
+
finally {
|
|
2968
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
});
|
|
2972
|
+
describe('Taskforce MCP document review tools', () => {
|
|
2973
|
+
it('gets one canonical document using D-123 identifiers', async () => {
|
|
2974
|
+
const projectRoot = createProjectRoot();
|
|
2975
|
+
try {
|
|
2976
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
2977
|
+
const core = new TaskforceCore({
|
|
2978
|
+
projectRoot,
|
|
2979
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
2980
|
+
});
|
|
2981
|
+
try {
|
|
2982
|
+
const task = core.createTask({ title: 'Document link fixture' }, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
|
|
2983
|
+
core.getWorkspaceAssetStore().upsertLink({
|
|
2984
|
+
workspaceId: 'workspace-1',
|
|
2985
|
+
assetId: asset.assetId,
|
|
2986
|
+
taskId: task.id,
|
|
2987
|
+
role: 'attachment',
|
|
2988
|
+
displayName: 'Linked rollout plan',
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
finally {
|
|
2992
|
+
core.close();
|
|
2993
|
+
}
|
|
2994
|
+
const result = parseToolJson(await callTool(projectRoot, 'local', 'get_document', {
|
|
2995
|
+
documentId: 'D-123',
|
|
2996
|
+
}, { workspaceId: 'workspace-1' }));
|
|
2997
|
+
expect(result.document).toMatchObject({
|
|
2998
|
+
assetId: asset.assetId,
|
|
2999
|
+
referenceLabel: 'D-123',
|
|
3000
|
+
apiUrl: `/api/taskforce/documents/${asset.assetId}/content`,
|
|
3001
|
+
});
|
|
3002
|
+
expect(result.linkedTasks).toHaveLength(1);
|
|
3003
|
+
expect(result.linkedTasks[0]).toMatchObject({
|
|
3004
|
+
caption: 'Linked rollout plan',
|
|
3005
|
+
});
|
|
3006
|
+
expect(result.reviewSessions).toHaveLength(1);
|
|
3007
|
+
expect(result.reviewSessions[0]).toMatchObject({
|
|
3008
|
+
title: 'Fixture review',
|
|
3009
|
+
status: 'open',
|
|
3010
|
+
commentCount: 1,
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
finally {
|
|
3014
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
it('lists and reads document review sessions using D-123 identifiers', async () => {
|
|
3018
|
+
const projectRoot = createProjectRoot();
|
|
3019
|
+
try {
|
|
3020
|
+
const { asset, session } = seedDocumentReviewFixture(projectRoot);
|
|
3021
|
+
const sessionsResult = parseToolJson(await callTool(projectRoot, 'local', 'get_document_review_sessions', {
|
|
3022
|
+
documentId: 'D-123',
|
|
3023
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3024
|
+
expect(sessionsResult.document).toMatchObject({
|
|
3025
|
+
assetId: asset.assetId,
|
|
3026
|
+
referenceLabel: 'D-123',
|
|
3027
|
+
});
|
|
3028
|
+
expect(sessionsResult.sessions).toHaveLength(1);
|
|
3029
|
+
expect(sessionsResult.sessions[0]).toMatchObject({
|
|
3030
|
+
id: session.id,
|
|
3031
|
+
title: 'Fixture review',
|
|
3032
|
+
});
|
|
3033
|
+
const sessionResult = parseToolJson(await callTool(projectRoot, 'local', 'get_document_review_session', {
|
|
3034
|
+
sessionId: session.id,
|
|
3035
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3036
|
+
expect(sessionResult.document).toMatchObject({
|
|
3037
|
+
assetId: asset.assetId,
|
|
3038
|
+
referenceLabel: 'D-123',
|
|
3039
|
+
});
|
|
3040
|
+
expect(sessionResult.session.id).toBe(session.id);
|
|
3041
|
+
expect(sessionResult.session.comments[0]?.body).toBe('Please tighten the rollout section.');
|
|
3042
|
+
}
|
|
3043
|
+
finally {
|
|
3044
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
it('reads canonical document content directly through MCP in local and cloud runtimes', async () => {
|
|
3048
|
+
const projectRoot = createProjectRoot();
|
|
3049
|
+
try {
|
|
3050
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
3051
|
+
const localResult = parseToolJson(await callTool(projectRoot, 'local', 'read_document_content', {
|
|
3052
|
+
documentId: 'D-123',
|
|
3053
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3054
|
+
const cloudResult = parseToolJson(await callTool(projectRoot, 'cloud', 'read_document_content', {
|
|
3055
|
+
documentId: asset.documentId,
|
|
3056
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3057
|
+
expect(localResult.document).toMatchObject({
|
|
3058
|
+
assetId: asset.assetId,
|
|
3059
|
+
referenceLabel: 'D-123',
|
|
3060
|
+
});
|
|
3061
|
+
expect(localResult.format).toBe('text');
|
|
3062
|
+
expect(localResult.truncated).toBe(false);
|
|
3063
|
+
expect(localResult.content).toContain('# Fixture plan');
|
|
3064
|
+
expect(localResult.content).toContain('Please tighten the rollout section.');
|
|
3065
|
+
expect(cloudResult.document).toMatchObject({
|
|
3066
|
+
assetId: asset.assetId,
|
|
3067
|
+
referenceLabel: 'D-123',
|
|
3068
|
+
});
|
|
3069
|
+
expect(cloudResult.format).toBe('text');
|
|
3070
|
+
expect(cloudResult.content).toContain('# Fixture plan');
|
|
3071
|
+
}
|
|
3072
|
+
finally {
|
|
3073
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
it('searches canonical documents by reference label and filename fragment', async () => {
|
|
3077
|
+
const projectRoot = createProjectRoot();
|
|
3078
|
+
try {
|
|
3079
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
3080
|
+
const core = new TaskforceCore({
|
|
3081
|
+
projectRoot,
|
|
3082
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3083
|
+
});
|
|
3084
|
+
try {
|
|
3085
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
3086
|
+
const { basePath } = core.getPaths();
|
|
3087
|
+
const storageKey = 'workspaces/workspace-1/assets/documents/asset-document-2-architecture-notes.md';
|
|
3088
|
+
const absolutePath = path.join(basePath, storageKey);
|
|
3089
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
3090
|
+
fs.writeFileSync(absolutePath, '# Architecture notes\n\nSecondary fixture.\n', 'utf8');
|
|
3091
|
+
assetStore.upsertAsset({
|
|
3092
|
+
assetId: 'asset-document-2',
|
|
3093
|
+
workspaceId: 'workspace-1',
|
|
3094
|
+
kind: 'document',
|
|
3095
|
+
logicalName: 'Architecture notes',
|
|
3096
|
+
originalFilename: 'architecture-notes.md',
|
|
3097
|
+
mimeType: 'text/markdown',
|
|
3098
|
+
storageProvider: 'local',
|
|
3099
|
+
storageKey,
|
|
3100
|
+
contentSha256: 'sha-document-2',
|
|
3101
|
+
sizeBytes: 420,
|
|
3102
|
+
documentId: 'doc-architecture-2',
|
|
3103
|
+
referenceNumber: 124,
|
|
3104
|
+
version: 1,
|
|
3105
|
+
isLatest: true,
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
finally {
|
|
3109
|
+
core.close();
|
|
3110
|
+
}
|
|
3111
|
+
const byReference = parseToolJson(await callTool(projectRoot, 'cloud', 'search_documents', {
|
|
3112
|
+
query: 'D-123',
|
|
3113
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3114
|
+
expect(byReference.query).toBe('D-123');
|
|
3115
|
+
expect(byReference.results[0]).toMatchObject({
|
|
3116
|
+
document: {
|
|
3117
|
+
assetId: asset.assetId,
|
|
3118
|
+
referenceLabel: 'D-123',
|
|
3119
|
+
},
|
|
3120
|
+
nextTool: 'get_document',
|
|
3121
|
+
suggestedArgs: { documentId: 'D-123' },
|
|
3122
|
+
});
|
|
3123
|
+
expect(byReference.results[0]?.matchedOn).toContain('referenceLabel');
|
|
3124
|
+
expect(byReference.guidance).toContain('call get_document');
|
|
3125
|
+
const byFilename = parseToolJson(await callTool(projectRoot, 'cloud', 'search_documents', {
|
|
3126
|
+
query: 'architecture-notes',
|
|
3127
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3128
|
+
expect(byFilename.results[0]).toMatchObject({
|
|
3129
|
+
document: {
|
|
3130
|
+
assetId: 'asset-document-2',
|
|
3131
|
+
referenceLabel: 'D-124',
|
|
3132
|
+
},
|
|
3133
|
+
suggestedArgs: { documentId: 'D-124' },
|
|
3134
|
+
});
|
|
3135
|
+
expect(byFilename.results[0]?.matchedOn).toContain('originalFilename');
|
|
3136
|
+
}
|
|
3137
|
+
finally {
|
|
3138
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
it('nudges agents toward search_documents when exact document lookup fails', async () => {
|
|
3142
|
+
const projectRoot = createProjectRoot();
|
|
3143
|
+
try {
|
|
3144
|
+
seedDocumentReviewFixture(projectRoot);
|
|
3145
|
+
const result = await callTool(projectRoot, 'cloud', 'get_document', {
|
|
3146
|
+
documentId: 'rollout plan',
|
|
3147
|
+
}, { workspaceId: 'workspace-1' });
|
|
3148
|
+
expect(result.isError).toBe(true);
|
|
3149
|
+
expect(result.content?.[0]?.text).toContain('call search_documents first');
|
|
3150
|
+
}
|
|
3151
|
+
finally {
|
|
3152
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
it('tells agents exactly how to read document content from get_document', async () => {
|
|
3156
|
+
const projectRoot = createProjectRoot();
|
|
3157
|
+
try {
|
|
3158
|
+
seedDocumentReviewFixture(projectRoot);
|
|
3159
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'get_document', {
|
|
3160
|
+
documentId: 'D-123',
|
|
3161
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3162
|
+
expect(result.document.referenceLabel).toBe('D-123');
|
|
3163
|
+
expect(result.readContent).toMatchObject({
|
|
3164
|
+
available: true,
|
|
3165
|
+
nextTool: 'read_document_content',
|
|
3166
|
+
suggestedArgs: { documentId: 'D-123' },
|
|
3167
|
+
});
|
|
3168
|
+
expect(result.readContent.guidance).toContain('call read_document_content');
|
|
3169
|
+
expect(result.readContent.reviewNext).toMatchObject({
|
|
3170
|
+
helperTool: 'review_document',
|
|
3171
|
+
listTool: 'get_document_review_sessions',
|
|
3172
|
+
createTool: 'create_document_review_session',
|
|
3173
|
+
commentTool: 'add_document_review_comment',
|
|
3174
|
+
});
|
|
3175
|
+
expect(result.readContent.reviewNext.guidance).toContain('After reading the document body');
|
|
3176
|
+
}
|
|
3177
|
+
finally {
|
|
3178
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
it('creates, updates, and comments on document review sessions through MCP', async () => {
|
|
3182
|
+
const projectRoot = createProjectRoot();
|
|
3183
|
+
try {
|
|
3184
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
3185
|
+
const created = parseToolJson(await callTool(projectRoot, 'cloud', 'create_document_review_session', {
|
|
3186
|
+
documentId: asset.assetId,
|
|
3187
|
+
title: 'AI Review Round',
|
|
3188
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3189
|
+
expect(created.document.assetId).toBe(asset.assetId);
|
|
3190
|
+
expect(created.session.title).toBe('AI Review Round');
|
|
3191
|
+
expect(created.session.status).toBe('open');
|
|
3192
|
+
const updated = parseToolJson(await callTool(projectRoot, 'cloud', 'update_document_review_session', {
|
|
3193
|
+
sessionId: created.session.id,
|
|
3194
|
+
status: 'resolved',
|
|
3195
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3196
|
+
expect(updated.session.id).toBe(created.session.id);
|
|
3197
|
+
expect(updated.session.status).toBe('resolved');
|
|
3198
|
+
const clearedTitle = parseToolJson(await callTool(projectRoot, 'cloud', 'update_document_review_session', {
|
|
3199
|
+
sessionId: created.session.id,
|
|
3200
|
+
title: null,
|
|
3201
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3202
|
+
expect(clearedTitle.session.id).toBe(created.session.id);
|
|
3203
|
+
expect(clearedTitle.session.title).toBeNull();
|
|
3204
|
+
const commented = parseToolJson(await callTool(projectRoot, 'cloud', 'add_document_review_comment', {
|
|
3205
|
+
sessionId: created.session.id,
|
|
3206
|
+
body: 'Cloud reviewer follow-up.',
|
|
3207
|
+
anchor: { kind: 'document' },
|
|
3208
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3209
|
+
expect(commented.session.id).toBe(created.session.id);
|
|
3210
|
+
expect(commented.session.comments.at(-1)).toMatchObject({
|
|
3211
|
+
body: 'Cloud reviewer follow-up.',
|
|
3212
|
+
anchor: { kind: 'document' },
|
|
3213
|
+
});
|
|
3214
|
+
const updatedComment = parseToolJson(await callTool(projectRoot, 'cloud', 'update_document_review_comment', {
|
|
3215
|
+
sessionId: created.session.id,
|
|
3216
|
+
commentId: commented.session.comments.at(-1)?.id,
|
|
3217
|
+
body: 'Cloud reviewer edited follow-up.',
|
|
3218
|
+
anchor: null,
|
|
3219
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3220
|
+
expect(updatedComment.session.id).toBe(created.session.id);
|
|
3221
|
+
expect(updatedComment.session.comments.at(-1)).toMatchObject({
|
|
3222
|
+
body: 'Cloud reviewer edited follow-up.',
|
|
3223
|
+
anchor: null,
|
|
3224
|
+
});
|
|
3225
|
+
const deletedComment = parseToolJson(await callTool(projectRoot, 'cloud', 'delete_document_review_comment', {
|
|
3226
|
+
sessionId: created.session.id,
|
|
3227
|
+
commentId: commented.session.comments.at(-1)?.id,
|
|
3228
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3229
|
+
expect(deletedComment.session.id).toBe(created.session.id);
|
|
3230
|
+
expect(deletedComment.session.comments.some((comment) => comment.id === commented.session.comments.at(-1)?.id)).toBe(false);
|
|
3231
|
+
}
|
|
3232
|
+
finally {
|
|
3233
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
it('reviews a document through the helper by reusing a single open session', async () => {
|
|
3237
|
+
const projectRoot = createProjectRoot();
|
|
3238
|
+
try {
|
|
3239
|
+
const { asset, session } = seedDocumentReviewFixture(projectRoot);
|
|
3240
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'review_document', {
|
|
3241
|
+
documentId: 'D-123',
|
|
3242
|
+
comments: [
|
|
3243
|
+
{ body: 'Please add an owner fallback note.' },
|
|
3244
|
+
{ body: 'Clarify whether done remains active.' }
|
|
3245
|
+
],
|
|
3246
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3247
|
+
expect(result.document).toMatchObject({
|
|
3248
|
+
assetId: asset.assetId,
|
|
3249
|
+
referenceLabel: 'D-123',
|
|
3250
|
+
});
|
|
3251
|
+
expect(result.review.sessionId).toBe(session.id);
|
|
3252
|
+
expect(result.review.sessionAction).toBe('reused');
|
|
3253
|
+
expect(result.review.postedCommentIds).toHaveLength(2);
|
|
3254
|
+
expect(result.comments.map((comment) => comment.body)).toEqual([
|
|
3255
|
+
'Please add an owner fallback note.',
|
|
3256
|
+
'Clarify whether done remains active.'
|
|
3257
|
+
]);
|
|
3258
|
+
expect(result.session.comments.at(-1)?.body).toBe('Clarify whether done remains active.');
|
|
3259
|
+
}
|
|
3260
|
+
finally {
|
|
3261
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3262
|
+
}
|
|
3263
|
+
});
|
|
3264
|
+
it('creates a new review session through review_document when open-session reuse is ambiguous', async () => {
|
|
3265
|
+
const projectRoot = createProjectRoot();
|
|
3266
|
+
try {
|
|
3267
|
+
const { asset } = seedDocumentReviewFixture(projectRoot);
|
|
3268
|
+
const core = new TaskforceCore({
|
|
3269
|
+
projectRoot,
|
|
3270
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
3271
|
+
});
|
|
3272
|
+
core.createDocumentReviewSession({
|
|
3273
|
+
workspaceId: 'workspace-1',
|
|
3274
|
+
assetId: asset.assetId,
|
|
3275
|
+
title: 'Second open review',
|
|
3276
|
+
actorId: 'owner-1',
|
|
3277
|
+
});
|
|
3278
|
+
core.close();
|
|
3279
|
+
const result = parseToolJson(await callTool(projectRoot, 'cloud', 'review_document', {
|
|
3280
|
+
documentId: asset.documentId,
|
|
3281
|
+
title: 'Fresh helper review',
|
|
3282
|
+
comments: [{ body: 'Open sessions were ambiguous, so create a fresh one.' }],
|
|
3283
|
+
}, { workspaceId: 'workspace-1' }));
|
|
3284
|
+
expect(result.review.sessionAction).toBe('created');
|
|
3285
|
+
expect(result.review.postedCommentIds).toHaveLength(1);
|
|
3286
|
+
expect(result.session.title).toBe('Fresh helper review');
|
|
3287
|
+
expect(result.session.comments.at(-1)?.body).toBe('Open sessions were ambiguous, so create a fresh one.');
|
|
3288
|
+
}
|
|
3289
|
+
finally {
|
|
3290
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
});
|