@taskforcehq/taskforce 0.3.309 → 0.3.313

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.
Files changed (470) hide show
  1. package/README.md +89 -127
  2. package/dist/TaskforceCore.js +1 -1
  3. package/dist/components/context/ContextAttachmentManager.js +4 -3
  4. package/dist/components/features/InitiativesModule.js +1 -1
  5. package/dist/components/features/TaskSettings.js +1 -1
  6. package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
  7. package/dist/components/ui/DisclosureSectionHeader.js +8 -0
  8. package/dist/components/views/PlanComparisonPage.js +1 -1
  9. package/dist/components/views/StandaloneLayout.js +18 -4
  10. package/dist/components/views/panels/PlanningDrawer.js +2 -1
  11. package/dist/components/views/standalone/modals/AccountHubModal.js +1 -1
  12. package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
  13. package/dist/components/views/standalone/modals/HelpModal.js +17 -2
  14. package/dist/core/PlanEntitlementService.d.ts +2 -0
  15. package/dist/core/PlanEntitlementService.js +57 -4
  16. package/dist/hooks/useFilterSort.js +1 -1
  17. package/dist/hooks/useTaskforce.js +2 -2
  18. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +5 -2
  19. package/dist/localization/locales/en-US.js +1 -1
  20. package/dist/localization/locales/es-419.js +1 -1
  21. package/dist/localization/locales/pt-BR.js +1 -1
  22. package/dist/mcp/runtime.js +49 -14
  23. package/dist/server/routes/billing.js +0 -1
  24. package/dist/ui/assets/AgentsModule-BULqIo8e.js +1 -0
  25. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-DHr4rHK9.js} +1 -1
  26. package/dist/ui/assets/ContextAttachmentManager-DoMe2OpY.js +3 -0
  27. package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-DumBWhvb.js} +1 -1
  28. package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-sHk8Nzbq.js} +1 -1
  29. package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-3X9UNzHh.js} +1 -1
  30. package/dist/ui/assets/PlansPage-C3A3EYaV.js +1 -0
  31. package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
  32. package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-C8G7pWkX.js} +1 -1
  33. package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DM1o9Wla.js} +2 -2
  34. package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CW0crzvD.js} +1 -1
  35. package/dist/ui/assets/documentReferences-BZB3nMRb.js +1 -0
  36. package/dist/ui/assets/index-BWFQ9mGn.js +5 -0
  37. package/dist/ui/assets/{index-BvAbqx5Q.css → index-DcK_8tuE.css} +1 -1
  38. package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
  39. package/dist/ui/index.html +3 -3
  40. package/dist/utils/supportContact.d.ts +9 -0
  41. package/dist/utils/supportContact.js +32 -0
  42. package/package.json +12 -4
  43. package/scripts/postinstall.js +4 -3
  44. package/dist/TaskforceCore.CategoryFallback.test.d.ts +0 -1
  45. package/dist/TaskforceCore.CategoryFallback.test.js +0 -101
  46. package/dist/TaskforceCore.filter-persistence.test.d.ts +0 -1
  47. package/dist/TaskforceCore.filter-persistence.test.js +0 -402
  48. package/dist/TaskforceCore.routing.test.d.ts +0 -1
  49. package/dist/TaskforceCore.routing.test.js +0 -311
  50. package/dist/TaskforceCore.test.d.ts +0 -1
  51. package/dist/TaskforceCore.test.js +0 -7910
  52. package/dist/TaskforceWidget.test.d.ts +0 -1
  53. package/dist/TaskforceWidget.test.js +0 -92
  54. package/dist/UnsavedChangesRelaxed.test.d.ts +0 -1
  55. package/dist/UnsavedChangesRelaxed.test.js +0 -16
  56. package/dist/__tests__/localizationCatalogs.test.d.ts +0 -1
  57. package/dist/__tests__/localizationCatalogs.test.js +0 -54
  58. package/dist/__tests__/workflowSimulation.test.d.ts +0 -1
  59. package/dist/__tests__/workflowSimulation.test.js +0 -113
  60. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts +0 -1
  61. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +0 -278
  62. package/dist/annotatedAttachments/payload.test.d.ts +0 -1
  63. package/dist/annotatedAttachments/payload.test.js +0 -67
  64. package/dist/compat/workspaceSyncCompat.test.d.ts +0 -1
  65. package/dist/compat/workspaceSyncCompat.test.js +0 -124
  66. package/dist/components/features/AgentsModule.test.d.ts +0 -1
  67. package/dist/components/features/AgentsModule.test.js +0 -601
  68. package/dist/components/features/AnnotatedAttachmentWorkspace.test.d.ts +0 -1
  69. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +0 -2883
  70. package/dist/components/features/DocumentWorkspace.test.d.ts +0 -1
  71. package/dist/components/features/DocumentWorkspace.test.js +0 -1439
  72. package/dist/components/features/TaskSettings.test.d.ts +0 -1
  73. package/dist/components/features/TaskSettings.test.js +0 -1674
  74. package/dist/components/features/settings/TaxonomyLibraryManager.test.d.ts +0 -1
  75. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +0 -141
  76. package/dist/components/task/TaskActionHeader.test.d.ts +0 -1
  77. package/dist/components/task/TaskActionHeader.test.js +0 -91
  78. package/dist/components/task/TaskCard.test.d.ts +0 -1
  79. package/dist/components/task/TaskCard.test.js +0 -513
  80. package/dist/components/task/TaskContextUpload.test.d.ts +0 -1
  81. package/dist/components/task/TaskContextUpload.test.js +0 -506
  82. package/dist/components/task/TaskForm.test.d.ts +0 -1
  83. package/dist/components/task/TaskForm.test.js +0 -946
  84. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +0 -1
  85. package/dist/components/task/TaskKanban.empty-column-drop.test.js +0 -66
  86. package/dist/components/task/TaskKanban.planning-drop.test.d.ts +0 -1
  87. package/dist/components/task/TaskKanban.planning-drop.test.js +0 -93
  88. package/dist/components/task/TaskKanban.test.d.ts +0 -1
  89. package/dist/components/task/TaskKanban.test.js +0 -99
  90. package/dist/components/task/TaskList.test.d.ts +0 -1
  91. package/dist/components/task/TaskList.test.js +0 -473
  92. package/dist/components/task/TaskStatusActions.test.d.ts +0 -1
  93. package/dist/components/task/TaskStatusActions.test.js +0 -54
  94. package/dist/components/task/taskKanbanDropRules.test.d.ts +0 -1
  95. package/dist/components/task/taskKanbanDropRules.test.js +0 -114
  96. package/dist/components/ui/LevelSelector.test.d.ts +0 -1
  97. package/dist/components/ui/LevelSelector.test.js +0 -55
  98. package/dist/components/ui/Markdown.test.d.ts +0 -1
  99. package/dist/components/ui/Markdown.test.js +0 -38
  100. package/dist/components/ui/MultiSelectFilter.test.d.ts +0 -1
  101. package/dist/components/ui/MultiSelectFilter.test.js +0 -74
  102. package/dist/components/ui/TaxonomyDropdown.test.d.ts +0 -1
  103. package/dist/components/ui/TaxonomyDropdown.test.js +0 -130
  104. package/dist/components/ui/TopNoticeLayer.test.d.ts +0 -1
  105. package/dist/components/ui/TopNoticeLayer.test.js +0 -25
  106. package/dist/components/views/AppShellHeader.test.d.ts +0 -1
  107. package/dist/components/views/AppShellHeader.test.js +0 -42
  108. package/dist/components/views/PlanComparisonPage.test.d.ts +0 -1
  109. package/dist/components/views/PlanComparisonPage.test.js +0 -757
  110. package/dist/components/views/PlansPage.test.d.ts +0 -1
  111. package/dist/components/views/PlansPage.test.js +0 -710
  112. package/dist/components/views/chrome/WorkspaceChromeSections.test.d.ts +0 -1
  113. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +0 -54
  114. package/dist/components/views/chrome/workspaceChrome.test.d.ts +0 -1
  115. package/dist/components/views/chrome/workspaceChrome.test.js +0 -52
  116. package/dist/components/views/panels/DocumentFilterToolbar.test.d.ts +0 -1
  117. package/dist/components/views/panels/DocumentFilterToolbar.test.js +0 -19
  118. package/dist/components/views/panels/FilterToolbar.test.d.ts +0 -1
  119. package/dist/components/views/panels/FilterToolbar.test.js +0 -47
  120. package/dist/components/views/panels/PlanningDrawer.test.d.ts +0 -1
  121. package/dist/components/views/panels/PlanningDrawer.test.js +0 -529
  122. package/dist/components/views/planningScope.test.d.ts +0 -1
  123. package/dist/components/views/planningScope.test.js +0 -60
  124. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +0 -1
  125. package/dist/components/views/standalone/modals/AccountHubModal.test.js +0 -66
  126. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +0 -1
  127. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +0 -24
  128. package/dist/components/views/teamManagementAccess.test.d.ts +0 -1
  129. package/dist/components/views/teamManagementAccess.test.js +0 -44
  130. package/dist/config/deployment.test.d.ts +0 -1
  131. package/dist/config/deployment.test.js +0 -90
  132. package/dist/config/envSchema.test.d.ts +0 -1
  133. package/dist/config/envSchema.test.js +0 -125
  134. package/dist/core/AiProfileService.test.d.ts +0 -1
  135. package/dist/core/AiProfileService.test.js +0 -184
  136. package/dist/core/AiProfiles.test.d.ts +0 -1
  137. package/dist/core/AiProfiles.test.js +0 -1873
  138. package/dist/core/AuthTokenService.test.d.ts +0 -1
  139. package/dist/core/AuthTokenService.test.js +0 -78
  140. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +0 -1
  141. package/dist/core/BulkUpdateFieldsPreview.test.js +0 -62
  142. package/dist/core/CompletedAt.test.d.ts +0 -1
  143. package/dist/core/CompletedAt.test.js +0 -44
  144. package/dist/core/CreatedBy.test.d.ts +0 -1
  145. package/dist/core/CreatedBy.test.js +0 -188
  146. package/dist/core/DeletedTaskTrash.test.d.ts +0 -1
  147. package/dist/core/DeletedTaskTrash.test.js +0 -104
  148. package/dist/core/EntitlementsPolicy.test.d.ts +0 -1
  149. package/dist/core/EntitlementsPolicy.test.js +0 -280
  150. package/dist/core/GlobalSettingsService.test.d.ts +0 -1
  151. package/dist/core/GlobalSettingsService.test.js +0 -45
  152. package/dist/core/JsonBackupRuntime.test.d.ts +0 -1
  153. package/dist/core/JsonBackupRuntime.test.js +0 -155
  154. package/dist/core/PlanFeatureCatalog.test.d.ts +0 -1
  155. package/dist/core/PlanFeatureCatalog.test.js +0 -139
  156. package/dist/core/PlanVersionPolicy.test.d.ts +0 -1
  157. package/dist/core/PlanVersionPolicy.test.js +0 -204
  158. package/dist/core/PlanningEntities.test.d.ts +0 -1
  159. package/dist/core/PlanningEntities.test.js +0 -509
  160. package/dist/core/ScheduleFields.test.d.ts +0 -1
  161. package/dist/core/ScheduleFields.test.js +0 -193
  162. package/dist/core/SetupState.test.d.ts +0 -1
  163. package/dist/core/SetupState.test.js +0 -113
  164. package/dist/core/SignupMonetizationSettings.test.d.ts +0 -1
  165. package/dist/core/SignupMonetizationSettings.test.js +0 -515
  166. package/dist/core/Stats.test.d.ts +0 -1
  167. package/dist/core/Stats.test.js +0 -36
  168. package/dist/core/SystemAdmin.test.d.ts +0 -1
  169. package/dist/core/SystemAdmin.test.js +0 -1106
  170. package/dist/core/TaskActivitySync.test.d.ts +0 -1
  171. package/dist/core/TaskActivitySync.test.js +0 -90
  172. package/dist/core/TaskAttachmentsCanonical.test.d.ts +0 -1
  173. package/dist/core/TaskAttachmentsCanonical.test.js +0 -356
  174. package/dist/core/TaskAuditTimeline.test.d.ts +0 -1
  175. package/dist/core/TaskAuditTimeline.test.js +0 -319
  176. package/dist/core/TaskTaxonomyValidation.test.d.ts +0 -1
  177. package/dist/core/TaskTaxonomyValidation.test.js +0 -440
  178. package/dist/core/Taskforce.ids.test.d.ts +0 -1
  179. package/dist/core/Taskforce.ids.test.js +0 -611
  180. package/dist/core/Taskforce.listTasksSlim.test.d.ts +0 -1
  181. package/dist/core/Taskforce.listTasksSlim.test.js +0 -91
  182. package/dist/core/Taskforce.mcpAuth.test.d.ts +0 -1
  183. package/dist/core/Taskforce.mcpAuth.test.js +0 -450
  184. package/dist/core/Taskforce.realtimeMutation.test.d.ts +0 -1
  185. package/dist/core/Taskforce.realtimeMutation.test.js +0 -131
  186. package/dist/core/UserProfileAvatarDrafts.test.d.ts +0 -1
  187. package/dist/core/UserProfileAvatarDrafts.test.js +0 -138
  188. package/dist/core/WorkspaceDelete.test.d.ts +0 -1
  189. package/dist/core/WorkspaceDelete.test.js +0 -159
  190. package/dist/core/WorkspacePermissions.test.d.ts +0 -1
  191. package/dist/core/WorkspacePermissions.test.js +0 -270
  192. package/dist/core/WorkspacePlanMode.test.d.ts +0 -1
  193. package/dist/core/WorkspacePlanMode.test.js +0 -383
  194. package/dist/core/WorkspaceSyncSettings.test.d.ts +0 -1
  195. package/dist/core/WorkspaceSyncSettings.test.js +0 -65
  196. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +0 -1
  197. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +0 -212
  198. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +0 -1
  199. package/dist/documentReviews/DocumentReviewSessionStore.test.js +0 -127
  200. package/dist/hooks/authGuardPolicy.test.d.ts +0 -1
  201. package/dist/hooks/authGuardPolicy.test.js +0 -76
  202. package/dist/hooks/bootstrapTimeouts.test.d.ts +0 -1
  203. package/dist/hooks/bootstrapTimeouts.test.js +0 -12
  204. package/dist/hooks/sync/controlPlane.test.d.ts +0 -1
  205. package/dist/hooks/sync/controlPlane.test.js +0 -121
  206. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +0 -1
  207. package/dist/hooks/sync/lifecyclePolicy.test.js +0 -124
  208. package/dist/hooks/sync/orchestratorShared.test.d.ts +0 -1
  209. package/dist/hooks/sync/orchestratorShared.test.js +0 -98
  210. package/dist/hooks/sync/pullLifecycle.test.d.ts +0 -1
  211. package/dist/hooks/sync/pullLifecycle.test.js +0 -80
  212. package/dist/hooks/sync/recovery.test.d.ts +0 -1
  213. package/dist/hooks/sync/recovery.test.js +0 -292
  214. package/dist/hooks/sync/transfers.test.d.ts +0 -1
  215. package/dist/hooks/sync/transfers.test.js +0 -928
  216. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +0 -1
  217. package/dist/hooks/sync/useRecentSyncEvents.test.js +0 -124
  218. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +0 -1
  219. package/dist/hooks/sync/useSyncStatusActions.test.js +0 -133
  220. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +0 -1
  221. package/dist/hooks/sync/useSyncStatusControls.test.js +0 -88
  222. package/dist/hooks/useFilterSort.test.d.ts +0 -1
  223. package/dist/hooks/useFilterSort.test.js +0 -419
  224. package/dist/hooks/useRealtimeSync.test.d.ts +0 -1
  225. package/dist/hooks/useRealtimeSync.test.js +0 -311
  226. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +0 -1
  227. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +0 -54
  228. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +0 -1
  229. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +0 -228
  230. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +0 -1
  231. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +0 -2027
  232. package/dist/hooks/useTaskData.test.d.ts +0 -1
  233. package/dist/hooks/useTaskData.test.js +0 -291
  234. package/dist/hooks/useTaskMutations.test.d.ts +0 -1
  235. package/dist/hooks/useTaskMutations.test.js +0 -256
  236. package/dist/hooks/useTaskforce.contract.test.d.ts +0 -1
  237. package/dist/hooks/useTaskforce.contract.test.js +0 -7
  238. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +0 -1
  239. package/dist/hooks/useTaskforce.priority-filters.test.js +0 -28
  240. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +0 -1
  241. package/dist/hooks/useTaskforce.runtime-routing.test.js +0 -246
  242. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +0 -1
  243. package/dist/hooks/useTaskforce.sync-behavior.test.js +0 -3644
  244. package/dist/hooks/useWorkspaceSyncController.test.d.ts +0 -1
  245. package/dist/hooks/useWorkspaceSyncController.test.js +0 -192
  246. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +0 -1
  247. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +0 -77
  248. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +0 -1
  249. package/dist/hooks/workspace/workspaceLocalState.test.js +0 -59
  250. package/dist/localization/localizationCatalogs.test.d.ts +0 -1
  251. package/dist/localization/localizationCatalogs.test.js +0 -37
  252. package/dist/mcp/aiProfileBootstrap.test.d.ts +0 -1
  253. package/dist/mcp/aiProfileBootstrap.test.js +0 -68
  254. package/dist/mcp/canonicalAssetHelpers.test.d.ts +0 -1
  255. package/dist/mcp/canonicalAssetHelpers.test.js +0 -67
  256. package/dist/mcp/clientIntegrations.test.d.ts +0 -1
  257. package/dist/mcp/clientIntegrations.test.js +0 -219
  258. package/dist/mcp/optionResolution.test.d.ts +0 -1
  259. package/dist/mcp/optionResolution.test.js +0 -43
  260. package/dist/mcp/runtime.test.d.ts +0 -1
  261. package/dist/mcp/runtime.test.js +0 -5120
  262. package/dist/mcp/structuredCommentValidation.test.d.ts +0 -1
  263. package/dist/mcp/structuredCommentValidation.test.js +0 -31
  264. package/dist/mcp/structuredDocValidation.test.d.ts +0 -1
  265. package/dist/mcp/structuredDocValidation.test.js +0 -35
  266. package/dist/mcp/toolRegistry.test.d.ts +0 -1
  267. package/dist/mcp/toolRegistry.test.js +0 -34
  268. package/dist/migrations/billingSchemaParity.test.d.ts +0 -1
  269. package/dist/migrations/billingSchemaParity.test.js +0 -160
  270. package/dist/migrations/taskSchemaMigrations.test.d.ts +0 -1
  271. package/dist/migrations/taskSchemaMigrations.test.js +0 -225
  272. package/dist/qaOpen.test.d.ts +0 -1
  273. package/dist/qaOpen.test.js +0 -56
  274. package/dist/releaseVersion.test.d.ts +0 -1
  275. package/dist/releaseVersion.test.js +0 -32
  276. package/dist/runtime/appGates.test.d.ts +0 -1
  277. package/dist/runtime/appGates.test.js +0 -42
  278. package/dist/server/aiProfileAvatarAssets.test.d.ts +0 -1
  279. package/dist/server/aiProfileAvatarAssets.test.js +0 -213
  280. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +0 -1
  281. package/dist/server/annotatedAttachmentsRoutes.test.js +0 -532
  282. package/dist/server/auth.test.d.ts +0 -1
  283. package/dist/server/auth.test.js +0 -131
  284. package/dist/server/buildInfo.test.d.ts +0 -1
  285. package/dist/server/buildInfo.test.js +0 -29
  286. package/dist/server/cors.test.d.ts +0 -1
  287. package/dist/server/cors.test.js +0 -77
  288. package/dist/server/documentReviewRoutes.test.d.ts +0 -1
  289. package/dist/server/documentReviewRoutes.test.js +0 -190
  290. package/dist/server/email.test.d.ts +0 -1
  291. package/dist/server/email.test.js +0 -113
  292. package/dist/server/index.cookieProxy.test.d.ts +0 -1
  293. package/dist/server/index.cookieProxy.test.js +0 -67
  294. package/dist/server/index.rateLimit.test.d.ts +0 -1
  295. package/dist/server/index.rateLimit.test.js +0 -22
  296. package/dist/server/index.test.d.ts +0 -1
  297. package/dist/server/index.test.js +0 -283
  298. package/dist/server/rateLimit.test.d.ts +0 -1
  299. package/dist/server/rateLimit.test.js +0 -107
  300. package/dist/server/realtimeSync.auth.test.d.ts +0 -1
  301. package/dist/server/realtimeSync.auth.test.js +0 -62
  302. package/dist/server/realtimeSync.replay.test.d.ts +0 -1
  303. package/dist/server/realtimeSync.replay.test.js +0 -192
  304. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +0 -1
  305. package/dist/server/realtimeSync.workspaceIsolation.test.js +0 -101
  306. package/dist/server/routes/billing.test.d.ts +0 -1
  307. package/dist/server/routes/billing.test.js +0 -3455
  308. package/dist/server/routes/shared.test.d.ts +0 -1
  309. package/dist/server/routes/shared.test.js +0 -32
  310. package/dist/server/routes/sync.integration.test.d.ts +0 -1
  311. package/dist/server/routes/sync.integration.test.js +0 -2932
  312. package/dist/server/routes/syncAuxRoutes.test.d.ts +0 -1
  313. package/dist/server/routes/syncAuxRoutes.test.js +0 -158
  314. package/dist/server/routes/syncRouteGuards.test.d.ts +0 -1
  315. package/dist/server/routes/syncRouteGuards.test.js +0 -94
  316. package/dist/server/routes.test.d.ts +0 -1
  317. package/dist/server/routes.test.js +0 -15612
  318. package/dist/server/runtimeMode.test.d.ts +0 -1
  319. package/dist/server/runtimeMode.test.js +0 -26
  320. package/dist/server/securityHeaders.test.d.ts +0 -1
  321. package/dist/server/securityHeaders.test.js +0 -70
  322. package/dist/services/templateEngine.test.d.ts +0 -1
  323. package/dist/services/templateEngine.test.js +0 -150
  324. package/dist/services/templates.test.d.ts +0 -1
  325. package/dist/services/templates.test.js +0 -45
  326. package/dist/services/workflowDocumentFixtures.test.d.ts +0 -1
  327. package/dist/services/workflowDocumentFixtures.test.js +0 -78
  328. package/dist/services/workflowExportMatrix.test.d.ts +0 -1
  329. package/dist/services/workflowExportMatrix.test.js +0 -113
  330. package/dist/services/workflowExportSnapshots.test.d.ts +0 -1
  331. package/dist/services/workflowExportSnapshots.test.js +0 -30
  332. package/dist/services/workflowGeneration.test.d.ts +0 -1
  333. package/dist/services/workflowGeneration.test.js +0 -12
  334. package/dist/services/workflowSimulation.test.d.ts +0 -1
  335. package/dist/services/workflowSimulation.test.js +0 -113
  336. package/dist/services/workflowV2Policy.test.d.ts +0 -1
  337. package/dist/services/workflowV2Policy.test.js +0 -80
  338. package/dist/shared/customTaxonomyLibrary.test.d.ts +0 -1
  339. package/dist/shared/customTaxonomyLibrary.test.js +0 -20
  340. package/dist/shared/passwordPolicy.test.d.ts +0 -1
  341. package/dist/shared/passwordPolicy.test.js +0 -34
  342. package/dist/shared/runtimeContract.test.d.ts +0 -1
  343. package/dist/shared/runtimeContract.test.js +0 -23
  344. package/dist/shared/taxonomyLibrary.test.d.ts +0 -1
  345. package/dist/shared/taxonomyLibrary.test.js +0 -26
  346. package/dist/shared/taxonomyState.test.d.ts +0 -1
  347. package/dist/shared/taxonomyState.test.js +0 -71
  348. package/dist/soak/preflight.cli.d.ts +0 -1
  349. package/dist/soak/preflight.cli.js +0 -28
  350. package/dist/soak/preflight.d.ts +0 -26
  351. package/dist/soak/preflight.js +0 -113
  352. package/dist/soak/preflight.test.d.ts +0 -1
  353. package/dist/soak/preflight.test.js +0 -39
  354. package/dist/storage/objectStorageClient.test.d.ts +0 -1
  355. package/dist/storage/objectStorageClient.test.js +0 -82
  356. package/dist/storage/objectStorageConfig.test.d.ts +0 -1
  357. package/dist/storage/objectStorageConfig.test.js +0 -50
  358. package/dist/storage/postgresAdapter.test.d.ts +0 -1
  359. package/dist/storage/postgresAdapter.test.js +0 -29
  360. package/dist/storage/workspaceAssetStore.test.d.ts +0 -1
  361. package/dist/storage/workspaceAssetStore.test.js +0 -588
  362. package/dist/sync/cloudSyncApi.test.d.ts +0 -1
  363. package/dist/sync/cloudSyncApi.test.js +0 -111
  364. package/dist/sync/collaborationSyncPayload.test.d.ts +0 -1
  365. package/dist/sync/collaborationSyncPayload.test.js +0 -125
  366. package/dist/sync/contentSyncPayload.test.d.ts +0 -1
  367. package/dist/sync/contentSyncPayload.test.js +0 -143
  368. package/dist/sync/encryptionService.test.d.ts +0 -1
  369. package/dist/sync/encryptionService.test.js +0 -52
  370. package/dist/sync/planningSyncPayload.test.d.ts +0 -1
  371. package/dist/sync/planningSyncPayload.test.js +0 -92
  372. package/dist/sync/realtimeFeature.test.d.ts +0 -1
  373. package/dist/sync/realtimeFeature.test.js +0 -30
  374. package/dist/sync/realtimeReconcile.test.d.ts +0 -1
  375. package/dist/sync/realtimeReconcile.test.js +0 -43
  376. package/dist/sync/syncApplyHandlers.test.d.ts +0 -1
  377. package/dist/sync/syncApplyHandlers.test.js +0 -626
  378. package/dist/sync/syncService.test.d.ts +0 -1
  379. package/dist/sync/syncService.test.js +0 -903
  380. package/dist/sync/taskSyncPayload.test.d.ts +0 -1
  381. package/dist/sync/taskSyncPayload.test.js +0 -54
  382. package/dist/sync/workspacePullFeed.test.d.ts +0 -1
  383. package/dist/sync/workspacePullFeed.test.js +0 -438
  384. package/dist/sync/workspaceSyncModel.test.d.ts +0 -1
  385. package/dist/sync/workspaceSyncModel.test.js +0 -1442
  386. package/dist/test/setup.d.ts +0 -1
  387. package/dist/test/setup.js +0 -15
  388. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
  389. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
  390. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
  391. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
  392. package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
  393. package/dist/ui/assets/index-MAHKvFqp.js +0 -4
  394. package/dist/utils/accountProfileSummaryCache.test.d.ts +0 -1
  395. package/dist/utils/accountProfileSummaryCache.test.js +0 -63
  396. package/dist/utils/annotatedAttachments.test.d.ts +0 -1
  397. package/dist/utils/annotatedAttachments.test.js +0 -108
  398. package/dist/utils/assignees.test.d.ts +0 -1
  399. package/dist/utils/assignees.test.js +0 -25
  400. package/dist/utils/avatarUpload.test.d.ts +0 -1
  401. package/dist/utils/avatarUpload.test.js +0 -40
  402. package/dist/utils/canonicalAssetKind.test.d.ts +0 -1
  403. package/dist/utils/canonicalAssetKind.test.js +0 -24
  404. package/dist/utils/coalescedRefresh.test.d.ts +0 -1
  405. package/dist/utils/coalescedRefresh.test.js +0 -30
  406. package/dist/utils/constants.icons.test.d.ts +0 -1
  407. package/dist/utils/constants.icons.test.js +0 -41
  408. package/dist/utils/planParser.test.d.ts +0 -1
  409. package/dist/utils/planParser.test.js +0 -183
  410. package/dist/utils/planningReferences.test.d.ts +0 -1
  411. package/dist/utils/planningReferences.test.js +0 -17
  412. package/dist/utils/syncEventDetails.test.d.ts +0 -1
  413. package/dist/utils/syncEventDetails.test.js +0 -23
  414. package/dist/utils/syncEventPresentation.test.d.ts +0 -1
  415. package/dist/utils/syncEventPresentation.test.js +0 -64
  416. package/dist/utils/syncStatusPresentation.test.d.ts +0 -1
  417. package/dist/utils/syncStatusPresentation.test.js +0 -164
  418. package/dist/utils/taskActivity.test.d.ts +0 -1
  419. package/dist/utils/taskActivity.test.js +0 -218
  420. package/dist/utils/taskDraftDefaults.test.d.ts +0 -1
  421. package/dist/utils/taskDraftDefaults.test.js +0 -34
  422. package/dist/utils/taskDragZones.test.d.ts +0 -1
  423. package/dist/utils/taskDragZones.test.js +0 -14
  424. package/dist/utils/taskNormalization.test.d.ts +0 -1
  425. package/dist/utils/taskNormalization.test.js +0 -158
  426. package/dist/utils/taskSearch.test.d.ts +0 -1
  427. package/dist/utils/taskSearch.test.js +0 -69
  428. package/dist/utils/taskUtils.test.d.ts +0 -1
  429. package/dist/utils/taskUtils.test.js +0 -19
  430. package/dist/utils/taskforceApiClient.test.d.ts +0 -1
  431. package/dist/utils/taskforceApiClient.test.js +0 -44
  432. package/dist/utils/taxonomyFormDefaults.test.d.ts +0 -1
  433. package/dist/utils/taxonomyFormDefaults.test.js +0 -63
  434. package/dist/utils/uiNotice.test.d.ts +0 -1
  435. package/dist/utils/uiNotice.test.js +0 -18
  436. package/dist/utils/workspaceIdentity.test.d.ts +0 -1
  437. package/dist/utils/workspaceIdentity.test.js +0 -61
  438. package/dist/utils/workspaceLifecycle.test.d.ts +0 -1
  439. package/dist/utils/workspaceLifecycle.test.js +0 -64
  440. package/dist/utils/workspaceSyncPresentation.test.d.ts +0 -1
  441. package/dist/utils/workspaceSyncPresentation.test.js +0 -71
  442. package/scripts/bootstrap-playwright-auth.mjs +0 -155
  443. package/scripts/check-cloud-mcp.mjs +0 -83
  444. package/scripts/check-compat-boundaries.mjs +0 -41
  445. package/scripts/check-env.ts +0 -31
  446. package/scripts/check-localization-strings.mjs +0 -49
  447. package/scripts/check-runtime-boundaries.mjs +0 -44
  448. package/scripts/debug-global.mjs +0 -34
  449. package/scripts/debug-login.mjs +0 -32
  450. package/scripts/debug-server.mjs +0 -36
  451. package/scripts/deploy-prod.sh +0 -41
  452. package/scripts/deploy-staging.sh +0 -23
  453. package/scripts/export-sqlite-to-postgres.mjs +0 -123
  454. package/scripts/generate-env-example.ts +0 -8
  455. package/scripts/mcp-stdio-adapter.mjs +0 -160
  456. package/scripts/migrate-task-data-assets.mjs +0 -408
  457. package/scripts/playwright-auth.sh +0 -19
  458. package/scripts/qa-open-lib.mjs +0 -175
  459. package/scripts/qa-open-lib.mjs.d.ts +0 -49
  460. package/scripts/qa-open.mjs +0 -352
  461. package/scripts/release-version-lib.mjs +0 -88
  462. package/scripts/release-version.mjs +0 -72
  463. package/scripts/run-billing-performance-smoke.sh +0 -24
  464. package/scripts/run-dev-proxy.sh +0 -51
  465. package/scripts/run-e2e-realtime.sh +0 -30
  466. package/scripts/run-smoke.sh +0 -35
  467. package/scripts/run-soak.sh +0 -41
  468. package/scripts/test-global.js +0 -143
  469. package/scripts/update-homebrew-formula.sh +0 -108
  470. package/scripts/with-esbuild.sh +0 -11
@@ -1,1873 +0,0 @@
1
- import fs from 'fs';
2
- import os from 'os';
3
- import path from 'path';
4
- import Database from 'better-sqlite3';
5
- import { afterEach, describe, expect, it } from 'vitest';
6
- import { TaskforceCore } from './Taskforce';
7
- function createProjectRoot(prefix = 'taskforce-ai-profiles-') {
8
- const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
9
- fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'ai-profiles-test' }), 'utf-8');
10
- return root;
11
- }
12
- const createdRoots = [];
13
- function enableAiProfileSeatLimit(core, workspaceId, userId, limit) {
14
- core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
15
- core.ensureBootstrapUserAccess({
16
- userId,
17
- workspaceId,
18
- role: 'owner',
19
- email: `${userId}@example.com`
20
- });
21
- core.updatePlanVersionFeatures({
22
- planVersionId: 'free-v1',
23
- features: [
24
- { featureKey: 'collaboration.ai_profiles', access: 'limited', config: { maxAiProfiles: limit } }
25
- ]
26
- });
27
- core.updateAccountEntitlement({
28
- accountId: userId,
29
- planVersionId: 'free-v1',
30
- entitlementState: 'active',
31
- actorUserId: 'system-admin',
32
- actorRole: 'system_admin'
33
- });
34
- }
35
- afterEach(() => {
36
- while (createdRoots.length > 0) {
37
- const root = createdRoots.pop();
38
- if (root && fs.existsSync(root)) {
39
- fs.rmSync(root, { recursive: true, force: true });
40
- }
41
- }
42
- });
43
- describe('TaskforceCore ai profiles', () => {
44
- it('creates workspace-scoped ai profiles and resolves them into task actor surfaces', () => {
45
- const projectRoot = createProjectRoot();
46
- createdRoots.push(projectRoot);
47
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
48
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
49
- const resolved = core.resolveAiProfile({
50
- workspaceId: 'workspace-1',
51
- name: 'Codex',
52
- icon: 'Bot',
53
- color: '#ef6c00'
54
- });
55
- expect(resolved.created).toBe(true);
56
- const actors = core.listAssignableActors('workspace-1');
57
- expect(actors.some((actor) => actor.id === resolved.profile.id && actor.label === 'Codex' && actor.kind === 'ai')).toBe(true);
58
- const task = core.createTask({
59
- title: 'Assigned to named AI',
60
- assignee: resolved.profile.id,
61
- createdBy: resolved.profile.id,
62
- comments: [
63
- {
64
- id: 'comment-1',
65
- author: resolved.profile.id,
66
- text: 'Hello from Codex',
67
- timestamp: '2026-03-22T10:00:00.000Z'
68
- }
69
- ]
70
- }, { workspaceId: 'workspace-1' });
71
- const fetched = core.getTask(task.id, 'workspace-1');
72
- expect(fetched?.assignee).toBe(resolved.profile.id);
73
- expect(fetched?.assigneeActor?.label).toBe('Codex');
74
- expect(fetched?.createdByActor?.label).toBe('Codex');
75
- expect(fetched?.comments?.[0]?.actor?.label).toBe('Codex');
76
- expect(fetched?.assigneeActor?.meta).not.toHaveProperty('profileToken');
77
- expect(fetched?.createdByActor?.meta).not.toHaveProperty('profileToken');
78
- expect(fetched?.comments?.[0]?.actor?.meta).not.toHaveProperty('profileToken');
79
- });
80
- it('updates existing ai profile presentation fields when profileToken is presented again', () => {
81
- const projectRoot = createProjectRoot();
82
- createdRoots.push(projectRoot);
83
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
84
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
85
- const first = core.resolveAiProfile({
86
- workspaceId: 'workspace-1',
87
- name: 'Claude',
88
- color: '#f59e0b'
89
- });
90
- const second = core.resolveAiProfile({
91
- workspaceId: 'workspace-1',
92
- name: 'Claude 2',
93
- icon: 'Sparkles',
94
- color: '#0ea5e9',
95
- profileToken: first.profile.profileToken
96
- });
97
- expect(second.created).toBe(false);
98
- expect(second.profile.id).toBe(first.profile.id);
99
- expect(second.profile.name).toBe('Claude 2');
100
- expect(second.profile.username).toBe('claude-2');
101
- expect(second.profile.icon).toBe('Sparkles');
102
- expect(second.profile.color).toBe('#0ea5e9');
103
- });
104
- it('updates ai profile avatar urls without changing the registered icon fallback', () => {
105
- const projectRoot = createProjectRoot();
106
- createdRoots.push(projectRoot);
107
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
108
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
109
- const resolved = core.resolveAiProfile({
110
- workspaceId: 'workspace-1',
111
- name: 'Codex',
112
- icon: 'Bot',
113
- color: '#0ea5e9'
114
- });
115
- const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
116
- const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
117
- const updated = core.updateAiProfileAvatar({
118
- workspaceId: 'workspace-1',
119
- profileId: resolved.profile.id,
120
- avatarUrl,
121
- avatarSourceUrl
122
- });
123
- expect(updated.avatarUrl).toBe(avatarUrl);
124
- expect(updated.avatarSourceUrl).toBe(avatarSourceUrl);
125
- expect(updated.avatarRevision).toBe(1);
126
- expect(updated.avatarUpdatedAt).toBe(updated.updatedAt);
127
- expect(updated.icon).toBe('Bot');
128
- expect(core.listAiProfiles('workspace-1')[0]?.avatarUrl).toBe(avatarUrl);
129
- expect(core.listAiProfiles('workspace-1')[0]?.avatarSourceUrl).toBe(avatarSourceUrl);
130
- expect(core.listAiProfiles('workspace-1')[0]?.avatarRevision).toBe(1);
131
- expect(core.listAiProfiles('workspace-1')[0]?.avatarUpdatedAt).toBe(updated.updatedAt);
132
- const cleared = core.updateAiProfileAvatar({
133
- workspaceId: 'workspace-1',
134
- profileId: resolved.profile.id,
135
- avatarUrl: null,
136
- avatarSourceUrl: null
137
- });
138
- expect(cleared.avatarUrl).toBeNull();
139
- expect(cleared.avatarSourceUrl).toBeNull();
140
- expect(cleared.avatarRevision).toBe(2);
141
- expect(cleared.avatarUpdatedAt).toBe(cleared.updatedAt);
142
- });
143
- it('seeds blank same-name runtime profiles when applying an ai profile avatar', () => {
144
- const projectRoot = createProjectRoot();
145
- createdRoots.push(projectRoot);
146
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
147
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
148
- const local = core.resolveAiProfile({
149
- workspaceId: 'workspace-1',
150
- name: 'Codex',
151
- icon: 'Bot',
152
- color: '#0ea5e9'
153
- }, { trustedSeatScope: 'local_unmetered' });
154
- const cloud = core.resolveAiProfile({
155
- workspaceId: 'workspace-1',
156
- name: 'Codex',
157
- icon: 'Bot',
158
- color: '#0ea5e9'
159
- }, { trustedSeatScope: 'cloud_metered' });
160
- const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
161
- const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
162
- const updated = core.updateAiProfileAvatar({
163
- workspaceId: 'workspace-1',
164
- profileId: local.profile.id,
165
- avatarUrl,
166
- avatarSourceUrl
167
- });
168
- const profiles = core.listAiProfiles('workspace-1');
169
- const refreshedLocal = profiles.find((profile) => profile.id === local.profile.id);
170
- const refreshedCloud = profiles.find((profile) => profile.id === cloud.profile.id);
171
- expect(refreshedLocal?.avatarUrl).toBe(avatarUrl);
172
- expect(refreshedCloud?.avatarUrl).toBe(avatarUrl);
173
- expect(refreshedCloud?.avatarSourceUrl).toBe(avatarSourceUrl);
174
- expect(refreshedCloud?.avatarRevision).toBe(updated.avatarRevision);
175
- expect(refreshedCloud?.avatarUpdatedAt).toBe(updated.avatarUpdatedAt);
176
- });
177
- it('repairs blank same-name runtime profile avatars on token lookup', () => {
178
- const projectRoot = createProjectRoot();
179
- createdRoots.push(projectRoot);
180
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
181
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
182
- const local = core.resolveAiProfile({
183
- workspaceId: 'workspace-1',
184
- name: 'Codex',
185
- icon: 'Bot',
186
- color: '#0ea5e9'
187
- }, { trustedSeatScope: 'local_unmetered' });
188
- const cloud = core.resolveAiProfile({
189
- workspaceId: 'workspace-1',
190
- name: 'Codex',
191
- icon: 'Bot',
192
- color: '#0ea5e9'
193
- }, { trustedSeatScope: 'cloud_metered' });
194
- const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
195
- const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
196
- const updated = core.updateAiProfileAvatar({
197
- workspaceId: 'workspace-1',
198
- profileId: local.profile.id,
199
- avatarUrl,
200
- avatarSourceUrl
201
- });
202
- core.db.prepare(`
203
- UPDATE ai_profiles
204
- SET avatar_url = NULL,
205
- avatar_source_url = NULL,
206
- avatar_revision = ?
207
- WHERE id = ?
208
- `).run(updated.avatarRevision, cloud.profile.id);
209
- const lookedUp = core.getAiProfileByToken({
210
- workspaceId: 'workspace-1',
211
- profileToken: cloud.profile.profileToken,
212
- seatScope: 'cloud_metered'
213
- });
214
- expect(lookedUp?.id).toBe(cloud.profile.id);
215
- expect(lookedUp?.avatarUrl).toBe(avatarUrl);
216
- expect(lookedUp?.avatarSourceUrl).toBe(avatarSourceUrl);
217
- expect(lookedUp?.avatarRevision).toBe(updated.avatarRevision);
218
- });
219
- it('preserves synced ai profile avatars when metadata-only sync payloads omit avatar fields', () => {
220
- const projectRoot = createProjectRoot();
221
- createdRoots.push(projectRoot);
222
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
223
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
224
- const resolved = core.resolveAiProfile({
225
- workspaceId: 'workspace-1',
226
- name: 'Codex',
227
- icon: 'Bot',
228
- color: '#0ea5e9'
229
- });
230
- const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
231
- const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
232
- const updated = core.updateAiProfileAvatar({
233
- workspaceId: 'workspace-1',
234
- profileId: resolved.profile.id,
235
- avatarUrl,
236
- avatarSourceUrl
237
- });
238
- const { avatarUrl: _avatarUrl, avatarSourceUrl: _avatarSourceUrl, ...metadataOnlyProfile } = updated;
239
- const metadataUpdatedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
240
- const replayed = core.upsertAiProfileForSync({
241
- ...metadataOnlyProfile,
242
- name: 'Codex Metadata Refresh',
243
- updatedAt: metadataUpdatedAt,
244
- lastActiveAt: metadataUpdatedAt
245
- });
246
- expect(replayed.name).toBe('Codex Metadata Refresh');
247
- expect(replayed.avatarUrl).toBe(avatarUrl);
248
- expect(replayed.avatarSourceUrl).toBe(avatarSourceUrl);
249
- });
250
- it('does not clear synced ai profile avatars from legacy stale null sync payloads', () => {
251
- const projectRoot = createProjectRoot();
252
- createdRoots.push(projectRoot);
253
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
254
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
255
- const resolved = core.resolveAiProfile({
256
- workspaceId: 'workspace-1',
257
- name: 'Codex',
258
- icon: 'Bot',
259
- color: '#0ea5e9'
260
- });
261
- const updated = core.updateAiProfileAvatar({
262
- workspaceId: 'workspace-1',
263
- profileId: resolved.profile.id,
264
- avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp',
265
- avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp'
266
- });
267
- const clearedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
268
- const { avatarRevision: _avatarRevision, avatarUpdatedAt: _avatarUpdatedAt, ...legacyUpdated } = updated;
269
- const cleared = core.upsertAiProfileForSync({
270
- ...legacyUpdated,
271
- avatarUrl: null,
272
- avatarSourceUrl: null,
273
- updatedAt: clearedAt,
274
- lastActiveAt: clearedAt
275
- });
276
- expect(cleared.avatarUrl).toBe(updated.avatarUrl);
277
- expect(cleared.avatarSourceUrl).toBe(updated.avatarSourceUrl);
278
- expect(cleared.avatarRevision).toBe(updated.avatarRevision);
279
- });
280
- it('does not clear synced ai profile avatars from blank revisions without avatar update timestamps', () => {
281
- const projectRoot = createProjectRoot();
282
- createdRoots.push(projectRoot);
283
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
284
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
285
- const resolved = core.resolveAiProfile({
286
- workspaceId: 'workspace-1',
287
- name: 'Codex',
288
- icon: 'Bot',
289
- color: '#0ea5e9'
290
- });
291
- const updated = core.updateAiProfileAvatar({
292
- workspaceId: 'workspace-1',
293
- profileId: resolved.profile.id,
294
- avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp',
295
- avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp'
296
- });
297
- const refreshedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
298
- const replayed = core.upsertAiProfileForSync({
299
- ...updated,
300
- avatarUrl: null,
301
- avatarSourceUrl: null,
302
- avatarRevision: (updated.avatarRevision ?? 0) + 1,
303
- avatarUpdatedAt: null,
304
- updatedAt: refreshedAt,
305
- lastActiveAt: refreshedAt
306
- });
307
- expect(replayed.avatarUrl).toBe(updated.avatarUrl);
308
- expect(replayed.avatarSourceUrl).toBe(updated.avatarSourceUrl);
309
- expect(replayed.avatarRevision).toBe(updated.avatarRevision);
310
- expect(replayed.avatarUpdatedAt).toBe(updated.avatarUpdatedAt);
311
- });
312
- it('clears synced ai profile avatars when sync payloads send a newer avatar revision', () => {
313
- const projectRoot = createProjectRoot();
314
- createdRoots.push(projectRoot);
315
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
316
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
317
- const resolved = core.resolveAiProfile({
318
- workspaceId: 'workspace-1',
319
- name: 'Codex',
320
- icon: 'Bot',
321
- color: '#0ea5e9'
322
- });
323
- const updated = core.updateAiProfileAvatar({
324
- workspaceId: 'workspace-1',
325
- profileId: resolved.profile.id,
326
- avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp',
327
- avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp'
328
- });
329
- const clearedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
330
- const cleared = core.upsertAiProfileForSync({
331
- ...updated,
332
- avatarUrl: null,
333
- avatarSourceUrl: null,
334
- avatarRevision: (updated.avatarRevision ?? 0) + 1,
335
- avatarUpdatedAt: clearedAt,
336
- updatedAt: clearedAt,
337
- lastActiveAt: clearedAt
338
- });
339
- expect(cleared.avatarUrl).toBeNull();
340
- expect(cleared.avatarSourceUrl).toBeNull();
341
- expect(cleared.avatarRevision).toBe((updated.avatarRevision ?? 0) + 1);
342
- expect(cleared.avatarUpdatedAt).toBe(clearedAt);
343
- });
344
- it('repairs blank legacy ai profile avatar pointers from workspace asset metadata', () => {
345
- const projectRoot = createProjectRoot();
346
- createdRoots.push(projectRoot);
347
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
348
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
349
- const resolved = core.resolveAiProfile({
350
- workspaceId: 'workspace-1',
351
- name: 'Felix Leiter',
352
- icon: 'Bot',
353
- color: '#0ea5e9'
354
- });
355
- const db = core.db;
356
- const croppedKey = `workspaces/workspace-1/assets/images/ai-profiles/${resolved.profile.id}/avatar/asset-cropped-Felix.webp`;
357
- const sourceKey = `workspaces/workspace-1/assets/images/ai-profiles/${resolved.profile.id}/avatar/asset-source-Felix.webp`;
358
- db.prepare(`
359
- INSERT INTO workspace_assets (
360
- asset_id, tenant_id, workspace_id, kind, logical_name, original_filename, mime_type,
361
- storage_provider, storage_key, content_sha256, size_bytes, is_latest, scan_status,
362
- created_at, updated_at
363
- ) VALUES (?, 'default', 'workspace-1', 'image', ?, ?, 'image/webp', 'local', ?, ?, 100, 1, 'clean', ?, ?)
364
- `).run('asset-cropped', 'Felix avatar', 'cropped-Felix.webp', croppedKey, 'sha-cropped', '2026-04-28T15:00:00.000Z', '2026-04-28T15:00:00.000Z');
365
- db.prepare(`
366
- INSERT INTO workspace_assets (
367
- asset_id, tenant_id, workspace_id, kind, logical_name, original_filename, mime_type,
368
- storage_provider, storage_key, content_sha256, size_bytes, is_latest, scan_status,
369
- created_at, updated_at
370
- ) VALUES (?, 'default', 'workspace-1', 'image', ?, ?, 'image/webp', 'local', ?, ?, 100, 1, 'clean', ?, ?)
371
- `).run('asset-source', 'Felix source', 'source-Felix.webp', sourceKey, 'sha-source', '2026-04-28T15:01:00.000Z', '2026-04-28T15:01:00.000Z');
372
- const repaired = core.listAiProfiles('workspace-1').find((profile) => profile.id === resolved.profile.id);
373
- expect(repaired?.avatarUrl).toBe(`/api/taskforce/context/${encodeURIComponent(croppedKey)}`);
374
- expect(repaired?.avatarSourceUrl).toBe(`/api/taskforce/context/${encodeURIComponent(sourceKey)}`);
375
- expect(repaired?.avatarRevision).toBe(1);
376
- expect(repaired?.avatarUpdatedAt).toBe('2026-04-28T15:00:00.000Z');
377
- core.updateAiProfileAvatar({
378
- workspaceId: 'workspace-1',
379
- profileId: resolved.profile.id,
380
- avatarUrl: null,
381
- avatarSourceUrl: null
382
- });
383
- const cleared = core.listAiProfiles('workspace-1').find((profile) => profile.id === resolved.profile.id);
384
- expect(cleared?.avatarUrl).toBeNull();
385
- expect(cleared?.avatarSourceUrl).toBeNull();
386
- });
387
- it('repairs missing synced ai profile avatars when timestamps match', () => {
388
- const projectRoot = createProjectRoot();
389
- createdRoots.push(projectRoot);
390
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
391
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
392
- const resolved = core.resolveAiProfile({
393
- workspaceId: 'workspace-1',
394
- name: 'Codex',
395
- icon: 'Bot',
396
- color: '#0ea5e9'
397
- });
398
- const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
399
- const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
400
- const updated = core.updateAiProfileAvatar({
401
- workspaceId: 'workspace-1',
402
- profileId: resolved.profile.id,
403
- avatarUrl,
404
- avatarSourceUrl
405
- });
406
- core.db.prepare(`
407
- UPDATE ai_profiles
408
- SET avatar_url = NULL,
409
- avatar_source_url = NULL
410
- WHERE id = ?
411
- `).run(updated.id);
412
- const repaired = core.upsertAiProfileForSync(updated);
413
- expect(repaired.updatedAt).toBe(updated.updatedAt);
414
- expect(repaired.avatarUrl).toBe(avatarUrl);
415
- expect(repaired.avatarSourceUrl).toBe(avatarSourceUrl);
416
- });
417
- it('stores expanded ai profile fields and preserves them when omitted on re-resolve', () => {
418
- const projectRoot = createProjectRoot();
419
- createdRoots.push(projectRoot);
420
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
421
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
422
- const first = core.resolveAiProfile({
423
- workspaceId: 'workspace-1',
424
- name: 'Codex',
425
- description: 'Builds and repairs product code',
426
- role: 'Implementer',
427
- provider: 'OpenAI',
428
- model: 'gpt-5.4',
429
- surfaceType: 'coding_tool'
430
- });
431
- const second = core.resolveAiProfile({
432
- workspaceId: 'workspace-1',
433
- name: 'Codex',
434
- color: '#0ea5e9',
435
- profileToken: first.profile.profileToken
436
- });
437
- expect(second.profile.description).toBe('Builds and repairs product code');
438
- expect(second.profile.role).toBe('Implementer');
439
- expect(second.profile.provider).toBe('OpenAI');
440
- expect(second.profile.model).toBe('gpt-5.4');
441
- expect(second.profile.surfaceType).toBe('coding_tool');
442
- expect(second.profile.color).toBe('#0ea5e9');
443
- });
444
- it('applies default metadata for named agent profiles when optional fields are omitted', () => {
445
- const projectRoot = createProjectRoot();
446
- createdRoots.push(projectRoot);
447
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
448
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
449
- const resolved = core.resolveAiProfile({
450
- workspaceId: 'workspace-1',
451
- name: 'John Drake',
452
- surfaceType: 'agent'
453
- });
454
- expect(resolved.profile.description).toBe('John Drake AI collaborator registered in Taskforce.');
455
- expect(resolved.profile.role).toBe('AI Collaborator');
456
- expect(resolved.profile.provider).toBeNull();
457
- expect(resolved.profile.model).toBeNull();
458
- expect(resolved.profile.surfaceType).toBe('agent');
459
- });
460
- it('self-heals sparse named agent profiles on re-resolve', () => {
461
- const projectRoot = createProjectRoot();
462
- createdRoots.push(projectRoot);
463
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
464
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
465
- const first = core.resolveAiProfile({
466
- workspaceId: 'workspace-1',
467
- name: 'John Drake'
468
- });
469
- expect(first.profile.description).toBeNull();
470
- expect(first.profile.role).toBeNull();
471
- const second = core.resolveAiProfile({
472
- workspaceId: 'workspace-1',
473
- name: 'John Drake',
474
- surfaceType: 'agent',
475
- profileToken: first.profile.profileToken
476
- });
477
- expect(second.profile.id).toBe(first.profile.id);
478
- expect(second.profile.description).toBe('John Drake AI collaborator registered in Taskforce.');
479
- expect(second.profile.role).toBe('AI Collaborator');
480
- expect(second.profile.provider).toBeNull();
481
- expect(second.profile.model).toBeNull();
482
- expect(second.profile.surfaceType).toBe('agent');
483
- });
484
- it('durably repairs sparse named agent profiles when listing profiles', () => {
485
- const projectRoot = createProjectRoot();
486
- createdRoots.push(projectRoot);
487
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
488
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
489
- const first = core.resolveAiProfile({
490
- workspaceId: 'workspace-1',
491
- name: 'John Drake'
492
- });
493
- expect(first.profile.description).toBeNull();
494
- expect(first.profile.role).toBeNull();
495
- core.resolveAiProfile({
496
- workspaceId: 'workspace-1',
497
- name: 'John Drake',
498
- surfaceType: 'agent',
499
- profileToken: first.profile.profileToken
500
- });
501
- core.db.prepare(`
502
- UPDATE ai_profiles
503
- SET description = NULL, role = NULL, provider = NULL, model = NULL
504
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
505
- `).run('default', 'workspace-1', first.profile.id);
506
- const listed = core.listAiProfiles('workspace-1')[0];
507
- expect(listed.description).toBe('John Drake AI collaborator registered in Taskforce.');
508
- expect(listed.role).toBe('AI Collaborator');
509
- expect(listed.provider).toBeNull();
510
- expect(listed.model).toBeNull();
511
- const repaired = core.db.prepare(`
512
- SELECT description, role, provider, model
513
- FROM ai_profiles
514
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
515
- LIMIT 1
516
- `).get('default', 'workspace-1', first.profile.id);
517
- expect(repaired).toEqual({
518
- description: 'John Drake AI collaborator registered in Taskforce.',
519
- role: 'AI Collaborator',
520
- provider: null,
521
- model: null
522
- });
523
- });
524
- it('durably repairs sparse coding-tool profiles when resolving by token lookup', () => {
525
- const projectRoot = createProjectRoot();
526
- createdRoots.push(projectRoot);
527
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
528
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
529
- const resolved = core.resolveAiProfile({
530
- workspaceId: 'workspace-1',
531
- name: 'Codex',
532
- surfaceType: 'coding_tool'
533
- });
534
- core.db.prepare(`
535
- UPDATE ai_profiles
536
- SET description = NULL, role = NULL, provider = NULL, model = NULL
537
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
538
- `).run('default', 'workspace-1', resolved.profile.id);
539
- const fetched = core.getAiProfileByToken({
540
- workspaceId: 'workspace-1',
541
- profileToken: resolved.profile.profileToken
542
- });
543
- expect(fetched).toEqual(expect.objectContaining({
544
- description: 'Codex coding agent connected through MCP.',
545
- role: 'Coding Agent',
546
- provider: 'OpenAI',
547
- model: 'Codex',
548
- surfaceType: 'coding_tool'
549
- }));
550
- const repaired = core.db.prepare(`
551
- SELECT description, role, provider, model
552
- FROM ai_profiles
553
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
554
- LIMIT 1
555
- `).get('default', 'workspace-1', resolved.profile.id);
556
- expect(repaired).toEqual({
557
- description: 'Codex coding agent connected through MCP.',
558
- role: 'Coding Agent',
559
- provider: 'OpenAI',
560
- model: 'Codex'
561
- });
562
- });
563
- it('ignores empty-string optional ai profile updates instead of clearing stored values', () => {
564
- const projectRoot = createProjectRoot();
565
- createdRoots.push(projectRoot);
566
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
567
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
568
- const first = core.resolveAiProfile({
569
- workspaceId: 'workspace-1',
570
- name: 'Reviewer',
571
- description: 'Reviews implementation plans',
572
- role: 'Reviewer',
573
- provider: 'Anthropic',
574
- model: 'claude-sonnet'
575
- });
576
- const second = core.resolveAiProfile({
577
- workspaceId: 'workspace-1',
578
- name: 'Reviewer',
579
- description: ' ',
580
- role: '',
581
- provider: ' ',
582
- model: '',
583
- profileToken: first.profile.profileToken
584
- });
585
- expect(second.profile.description).toBe('Reviews implementation plans');
586
- expect(second.profile.role).toBe('Reviewer');
587
- expect(second.profile.provider).toBe('Anthropic');
588
- expect(second.profile.model).toBe('claude-sonnet');
589
- });
590
- it('preserves ai profile icon and color when re-resolved without those fields', () => {
591
- const projectRoot = createProjectRoot();
592
- createdRoots.push(projectRoot);
593
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
594
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
595
- const first = core.resolveAiProfile({
596
- workspaceId: 'workspace-1',
597
- name: 'Codex',
598
- icon: 'Sparkles',
599
- color: '#0ea5e9'
600
- });
601
- const second = core.resolveAiProfile({
602
- workspaceId: 'workspace-1',
603
- name: 'Codex 2',
604
- profileToken: first.profile.profileToken
605
- });
606
- expect(second.profile.icon).toBe('Sparkles');
607
- expect(second.profile.color).toBe('#0ea5e9');
608
- });
609
- it('clears ai profile surfaceType when re-resolved with null or an empty string', () => {
610
- const projectRoot = createProjectRoot();
611
- createdRoots.push(projectRoot);
612
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
613
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
614
- const first = core.resolveAiProfile({
615
- workspaceId: 'workspace-1',
616
- name: 'ChatGPT',
617
- surfaceType: 'chat_app'
618
- });
619
- const clearedWithNull = core.resolveAiProfile({
620
- workspaceId: 'workspace-1',
621
- name: 'ChatGPT',
622
- surfaceType: null,
623
- profileToken: first.profile.profileToken
624
- });
625
- expect(clearedWithNull.profile.surfaceType).toBeNull();
626
- const reset = core.resolveAiProfile({
627
- workspaceId: 'workspace-1',
628
- name: 'ChatGPT',
629
- surfaceType: 'chat_app',
630
- profileToken: first.profile.profileToken
631
- });
632
- expect(reset.profile.surfaceType).toBe('chat_app');
633
- const clearedWithEmptyString = core.resolveAiProfile({
634
- workspaceId: 'workspace-1',
635
- name: 'ChatGPT',
636
- surfaceType: '',
637
- profileToken: first.profile.profileToken
638
- });
639
- expect(clearedWithEmptyString.profile.surfaceType).toBeNull();
640
- });
641
- it('preserves ai profile surfaceType when re-resolved without the field', () => {
642
- const projectRoot = createProjectRoot();
643
- createdRoots.push(projectRoot);
644
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
645
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
646
- const first = core.resolveAiProfile({
647
- workspaceId: 'workspace-1',
648
- name: 'Codex',
649
- surfaceType: 'coding_tool'
650
- });
651
- const second = core.resolveAiProfile({
652
- workspaceId: 'workspace-1',
653
- name: 'Codex',
654
- profileToken: first.profile.profileToken
655
- });
656
- expect(second.profile.surfaceType).toBe('coding_tool');
657
- });
658
- it('supports cli and maps legacy other surfaceType values to agent', () => {
659
- const projectRoot = createProjectRoot();
660
- createdRoots.push(projectRoot);
661
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
662
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
663
- const cliProfile = core.resolveAiProfile({
664
- workspaceId: 'workspace-1',
665
- name: 'Codex',
666
- surfaceType: 'cli'
667
- });
668
- expect(cliProfile.profile.surfaceType).toBe('cli');
669
- expect(cliProfile.profile.role).toBe('CLI Coding Agent');
670
- const legacyOtherProfile = core.resolveAiProfile({
671
- workspaceId: 'workspace-1',
672
- name: 'Legacy Agent',
673
- surfaceType: 'other'
674
- });
675
- expect(legacyOtherProfile.profile.surfaceType).toBe('agent');
676
- });
677
- it('keeps a manually updated ai profile surfaceType from being overwritten by future registration', () => {
678
- const projectRoot = createProjectRoot();
679
- createdRoots.push(projectRoot);
680
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
681
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
682
- const first = core.resolveAiProfile({
683
- workspaceId: 'workspace-1',
684
- name: 'ChatGPT',
685
- surfaceType: 'chat_app'
686
- });
687
- const updated = core.updateAiProfileSurfaceType({
688
- workspaceId: 'workspace-1',
689
- profileId: first.profile.id,
690
- surfaceType: 'coding_tool'
691
- });
692
- expect(updated.surfaceType).toBe('coding_tool');
693
- const replayedRegistration = core.resolveAiProfile({
694
- workspaceId: 'workspace-1',
695
- name: 'ChatGPT',
696
- surfaceType: 'chat_app',
697
- profileToken: first.profile.profileToken
698
- });
699
- expect(replayedRegistration.profile.surfaceType).toBe('coding_tool');
700
- });
701
- it('treats unclassified ai profile category updates as clearing the manual override', () => {
702
- const projectRoot = createProjectRoot();
703
- createdRoots.push(projectRoot);
704
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
705
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
706
- const first = core.resolveAiProfile({
707
- workspaceId: 'workspace-1',
708
- name: 'ChatGPT',
709
- surfaceType: 'chat_app'
710
- });
711
- const cleared = core.updateAiProfileSurfaceType({
712
- workspaceId: 'workspace-1',
713
- profileId: first.profile.id,
714
- surfaceType: null
715
- });
716
- expect(cleared.surfaceType).toBeNull();
717
- const replayedRegistration = core.resolveAiProfile({
718
- workspaceId: 'workspace-1',
719
- name: 'ChatGPT',
720
- surfaceType: 'coding_tool',
721
- profileToken: first.profile.profileToken
722
- });
723
- expect(replayedRegistration.profile.surfaceType).toBe('coding_tool');
724
- });
725
- it('rejects invalid ai profile surfaceType values', () => {
726
- const projectRoot = createProjectRoot();
727
- createdRoots.push(projectRoot);
728
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
729
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
730
- expect(() => core.resolveAiProfile({
731
- workspaceId: 'workspace-1',
732
- name: 'Codex',
733
- surfaceType: 'desktop_app'
734
- })).toThrow(/surfaceType/i);
735
- });
736
- it('repairs legacy ai_profiles tables that are missing newer columns', () => {
737
- const projectRoot = createProjectRoot();
738
- createdRoots.push(projectRoot);
739
- const storageDir = path.join(projectRoot, '.taskforce');
740
- fs.mkdirSync(storageDir, { recursive: true });
741
- const dbPath = path.join(storageDir, 'taskforce.db');
742
- const db = new Database(dbPath);
743
- db.exec(`
744
- CREATE TABLE ai_profiles (
745
- id TEXT PRIMARY KEY,
746
- tenant_id TEXT NOT NULL DEFAULT 'default',
747
- workspace_id TEXT NOT NULL DEFAULT 'default',
748
- profile_token TEXT NOT NULL,
749
- name TEXT NOT NULL,
750
- username TEXT NOT NULL,
751
- icon TEXT NOT NULL,
752
- color TEXT NOT NULL,
753
- created_at TEXT NOT NULL,
754
- updated_at TEXT NOT NULL
755
- );
756
- `);
757
- db.prepare(`
758
- INSERT INTO ai_profiles (
759
- id, tenant_id, workspace_id, profile_token, name, username, icon, color, created_at, updated_at
760
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
761
- `).run('ai-profile-legacy-codex', 'default', 'workspace-1', 'tfp_legacy_codex', 'Codex', 'codex', 'Bot', '#0ea5e9', '2026-03-22T10:00:00.000Z', '2026-03-22T10:05:00.000Z');
762
- db.close();
763
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
764
- const profiles = core.listAiProfiles('workspace-1');
765
- expect(profiles).toEqual([
766
- expect.objectContaining({
767
- id: 'ai-profile-legacy-codex',
768
- name: 'Codex',
769
- username: 'codex',
770
- description: null,
771
- role: null,
772
- provider: null,
773
- model: null,
774
- surfaceType: null,
775
- lastActiveAt: null
776
- })
777
- ]);
778
- });
779
- it('orders ai profiles case-insensitively without sqlite-specific collation syntax', () => {
780
- const projectRoot = createProjectRoot();
781
- createdRoots.push(projectRoot);
782
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
783
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
784
- const sameTimestamp = '2026-03-22T12:00:00.000Z';
785
- const db = core.db;
786
- db.prepare(`
787
- INSERT INTO ai_profiles (
788
- id, tenant_id, workspace_id, profile_token, name, username, icon, color,
789
- provider_metadata_json, created_at, updated_at, last_active_at
790
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
791
- `).run('ai-profile-zebra', 'default', 'workspace-1', 'tfp_zebra', 'zebra', 'zebra', 'Bot', '#111111', null, sameTimestamp, sameTimestamp, null);
792
- db.prepare(`
793
- INSERT INTO ai_profiles (
794
- id, tenant_id, workspace_id, profile_token, name, username, icon, color,
795
- provider_metadata_json, created_at, updated_at, last_active_at
796
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
797
- `).run('ai-profile-alpha', 'default', 'workspace-1', 'tfp_alpha', 'Alpha', 'alpha', 'Bot', '#222222', null, sameTimestamp, sameTimestamp, null);
798
- db.prepare(`
799
- INSERT INTO ai_profiles (
800
- id, tenant_id, workspace_id, profile_token, name, username, icon, color,
801
- provider_metadata_json, created_at, updated_at, last_active_at
802
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
803
- `).run('ai-profile-bravo', 'default', 'workspace-1', 'tfp_bravo', 'bravo', 'bravo', 'Bot', '#333333', null, sameTimestamp, sameTimestamp, null);
804
- expect(core.listAiProfiles('workspace-1').map((profile) => profile.name)).toEqual([
805
- 'Alpha',
806
- 'bravo',
807
- 'zebra'
808
- ]);
809
- });
810
- it('uses a postgres-safe ordering expression for ai profiles', () => {
811
- const projectRoot = createProjectRoot();
812
- createdRoots.push(projectRoot);
813
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
814
- let capturedSql = '';
815
- core.db = {
816
- provider: 'postgres',
817
- prepare(sql) {
818
- capturedSql = sql;
819
- return {
820
- all() {
821
- return [];
822
- }
823
- };
824
- }
825
- };
826
- expect(core.listAiProfiles('workspace-1')).toEqual([]);
827
- expect(capturedSql).toContain('LOWER(name) ASC');
828
- expect(capturedSql).not.toContain('COLLATE NOCASE');
829
- });
830
- it('resolves actor by display name when comment author is a name rather than profile ID', () => {
831
- const projectRoot = createProjectRoot();
832
- createdRoots.push(projectRoot);
833
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
834
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
835
- const { profile } = core.resolveAiProfile({
836
- workspaceId: 'workspace-1',
837
- name: 'Claude',
838
- color: '#f59e0b'
839
- });
840
- // Resolve by name only when the alias is uniquely attributable.
841
- const actorByName = core.resolveWorkspaceActor('workspace-1', 'Claude');
842
- expect(actorByName).not.toBeNull();
843
- expect(actorByName?.id).toBe(profile.id);
844
- expect(actorByName?.kind).toBe('ai');
845
- expect(actorByName?.label).toBe('Claude');
846
- // Case-insensitive
847
- const actorLower = core.resolveWorkspaceActor('workspace-1', 'claude');
848
- expect(actorLower?.id).toBe(profile.id);
849
- });
850
- it('leaves duplicate ai profile aliases unresolved instead of picking one profile', () => {
851
- const projectRoot = createProjectRoot();
852
- createdRoots.push(projectRoot);
853
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
854
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
855
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude', color: '#f59e0b' });
856
- const second = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude 2', color: '#0ea5e9' });
857
- const db = core.db;
858
- db.prepare(`UPDATE ai_profiles SET name = 'Claude', username = 'claude' WHERE tenant_id = ? AND id = ?`)
859
- .run('default', second.profile.id);
860
- const actor = core.resolveWorkspaceActor('workspace-1', 'Claude');
861
- expect(actor?.id).toBe('Claude');
862
- expect(actor?.kind).toBe('ai');
863
- expect(actor?.meta?.unresolved).toBe(true);
864
- const task = core.createTask({
865
- title: 'Ambiguous legacy comment author',
866
- comments: [{
867
- id: 'comment-1',
868
- author: 'Claude',
869
- text: 'Keep the original ambiguous alias.',
870
- timestamp: '2026-03-22T10:00:00.000Z'
871
- }]
872
- }, { workspaceId: 'workspace-1' });
873
- const fetched = core.getTask(task.id, 'workspace-1');
874
- expect(fetched?.comments?.[0]?.author).toBe('Claude');
875
- expect(fetched?.comments?.[0]?.actor?.id).toBe('Claude');
876
- expect(fetched?.comments?.[0]?.actor?.meta?.unresolved).toBe(true);
877
- expect(first.profile.id).not.toBe(second.profile.id);
878
- });
879
- it('classifies well-known AI model names as ai-kind even without a registered profile', () => {
880
- const projectRoot = createProjectRoot();
881
- createdRoots.push(projectRoot);
882
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
883
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
884
- // No profile registered, but name is a known AI model
885
- const actor = core.resolveWorkspaceActor('workspace-1', 'chatgpt');
886
- expect(actor?.kind).toBe('ai');
887
- const actorCodex = core.resolveWorkspaceActor('workspace-1', 'codex');
888
- expect(actorCodex?.kind).toBe('ai');
889
- // Unknown string should default to human
890
- const actorUnknown = core.resolveWorkspaceActor('workspace-1', 'randomstring123');
891
- expect(actorUnknown?.kind).toBe('human');
892
- });
893
- it('reuses the existing profile when re-registering the same AI by exact name', () => {
894
- const projectRoot = createProjectRoot();
895
- createdRoots.push(projectRoot);
896
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
897
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
898
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity' });
899
- expect(first.created).toBe(true);
900
- expect(first.duplicateWarning).toBeUndefined();
901
- // Second creation without a token should reuse instead of minting a duplicate.
902
- const second = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity' });
903
- expect(second.created).toBe(false);
904
- expect(second.profile.id).toBe(first.profile.id);
905
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
906
- });
907
- it('reuses a stateless ai profile by name and provider while treating model changes as metadata updates', () => {
908
- const projectRoot = createProjectRoot();
909
- createdRoots.push(projectRoot);
910
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
911
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
912
- const first = core.resolveAiProfile({
913
- workspaceId: 'workspace-1',
914
- name: 'Claude',
915
- provider: 'Anthropic',
916
- model: 'claude-sonnet-4-5',
917
- surfaceType: 'chat_app'
918
- });
919
- const second = core.resolveAiProfile({
920
- workspaceId: 'workspace-1',
921
- name: 'claude',
922
- provider: 'anthropic',
923
- model: 'claude-sonnet-4-6',
924
- surfaceType: 'chat_app'
925
- });
926
- expect(second.created).toBe(false);
927
- expect(second.profile.id).toBe(first.profile.id);
928
- expect(second.profile.model).toBe('claude-sonnet-4-6');
929
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
930
- });
931
- it('reuses a provider-bearing stateless ai profile when a later resolve omits provider and model', () => {
932
- const projectRoot = createProjectRoot();
933
- createdRoots.push(projectRoot);
934
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
935
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
936
- const first = core.resolveAiProfile({
937
- workspaceId: 'workspace-1',
938
- name: 'Claude',
939
- provider: 'Anthropic',
940
- model: 'claude-sonnet-4-5',
941
- surfaceType: 'chat_app'
942
- });
943
- const second = core.resolveAiProfile({
944
- workspaceId: 'workspace-1',
945
- name: 'claude'
946
- });
947
- expect(second.created).toBe(false);
948
- expect(second.profile.id).toBe(first.profile.id);
949
- expect(second.profile.provider).toBe('Anthropic');
950
- expect(second.profile.model).toBe('claude-sonnet-4-5');
951
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
952
- });
953
- it('reuses same-scope ai profiles across provider drift', () => {
954
- const projectRoot = createProjectRoot();
955
- createdRoots.push(projectRoot);
956
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
957
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
958
- const original = core.resolveAiProfile({
959
- workspaceId: 'workspace-1',
960
- name: 'Felix Leiter',
961
- provider: 'OpenAI',
962
- model: 'openai-codex/gpt-5.4',
963
- surfaceType: 'agent'
964
- });
965
- const drifted = core.resolveAiProfile({
966
- workspaceId: 'workspace-1',
967
- name: 'Felix Leiter',
968
- provider: 'OpenClaw',
969
- model: 'gpt-5.4',
970
- surfaceType: 'agent'
971
- });
972
- expect(drifted.created).toBe(false);
973
- expect(drifted.profile.id).toBe(original.profile.id);
974
- expect(drifted.profile.provider).toBe('OpenClaw');
975
- expect(drifted.profile.model).toBe('gpt-5.4');
976
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
977
- });
978
- it('reuses same-scope ai profiles across surface type drift', () => {
979
- const projectRoot = createProjectRoot();
980
- createdRoots.push(projectRoot);
981
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
982
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
983
- const chat = core.resolveAiProfile({
984
- workspaceId: 'workspace-1',
985
- name: 'Claude',
986
- provider: 'Anthropic',
987
- model: 'claude-sonnet-4-6',
988
- surfaceType: 'chat_app'
989
- });
990
- const codingTool = core.resolveAiProfile({
991
- workspaceId: 'workspace-1',
992
- name: 'Claude',
993
- provider: 'Anthropic',
994
- model: 'claude-sonnet-4-6',
995
- surfaceType: 'coding_tool'
996
- });
997
- expect(codingTool.created).toBe(false);
998
- expect(codingTool.profile.id).toBe(chat.profile.id);
999
- expect(codingTool.profile.surfaceType).toBe('coding_tool');
1000
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1001
- });
1002
- it('reuses same-scope ai profiles when reconnecting with a stale token', () => {
1003
- const projectRoot = createProjectRoot();
1004
- createdRoots.push(projectRoot);
1005
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1006
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1007
- const original = core.resolveAiProfile({
1008
- workspaceId: 'workspace-1',
1009
- name: 'Tiffany Case',
1010
- provider: 'openai-codex',
1011
- model: 'gpt-5.4',
1012
- surfaceType: 'agent'
1013
- }, { trustedSeatScope: 'cloud_metered' });
1014
- const reconnected = core.resolveAiProfile({
1015
- workspaceId: 'workspace-1',
1016
- name: 'Tiffany Case',
1017
- profileToken: 'tfp_stale_tiffany_case_token',
1018
- provider: 'OpenAI Codex',
1019
- model: 'openai-codex/gpt-5.4',
1020
- surfaceType: 'agent'
1021
- }, { trustedSeatScope: 'cloud_metered' });
1022
- expect(reconnected.created).toBe(false);
1023
- expect(reconnected.profile.id).toBe(original.profile.id);
1024
- expect(reconnected.profile.provider).toBe('OpenAI Codex');
1025
- expect(reconnected.profile.model).toBe('openai-codex/gpt-5.4');
1026
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1027
- });
1028
- it('prefers the most recently active matching ai profile when stateless candidates are duplicated', () => {
1029
- const projectRoot = createProjectRoot();
1030
- createdRoots.push(projectRoot);
1031
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1032
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1033
- const older = core.resolveAiProfile({
1034
- workspaceId: 'workspace-1',
1035
- name: 'Claude',
1036
- provider: 'Anthropic',
1037
- model: 'claude-sonnet-4-5',
1038
- surfaceType: 'chat_app'
1039
- });
1040
- const newer = core.resolveAiProfile({
1041
- workspaceId: 'workspace-1',
1042
- name: 'Claude Duplicate',
1043
- provider: 'Anthropic',
1044
- model: 'claude-sonnet-4-6',
1045
- surfaceType: 'chat_app'
1046
- });
1047
- core.db.prepare(`
1048
- UPDATE ai_profiles
1049
- SET name = 'Claude',
1050
- username = 'claude',
1051
- last_active_at = CASE id
1052
- WHEN ? THEN '2026-04-01T00:00:00.000Z'
1053
- ELSE '2026-04-02T00:00:00.000Z'
1054
- END,
1055
- updated_at = CASE id
1056
- WHEN ? THEN '2026-04-01T00:00:00.000Z'
1057
- ELSE '2026-04-02T00:00:00.000Z'
1058
- END
1059
- WHERE id IN (?, ?)
1060
- `).run(older.profile.id, older.profile.id, older.profile.id, newer.profile.id);
1061
- const resolved = core.resolveAiProfile({
1062
- workspaceId: 'workspace-1',
1063
- name: 'Claude',
1064
- provider: 'Anthropic',
1065
- model: 'claude-sonnet-4-7',
1066
- surfaceType: 'chat_app'
1067
- });
1068
- expect(resolved.created).toBe(false);
1069
- expect(resolved.profile.id).toBe(newer.profile.id);
1070
- expect(resolved.duplicateWarning?.existingProfileId).toBe(newer.profile.id);
1071
- });
1072
- it('reuses Codex by exact name without leaking across workspaces', () => {
1073
- const projectRoot = createProjectRoot();
1074
- createdRoots.push(projectRoot);
1075
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1076
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1077
- core.createWorkspace({ workspaceId: 'workspace-2', name: 'Workspace 2' });
1078
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' });
1079
- const second = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' });
1080
- const otherWorkspace = core.resolveAiProfile({ workspaceId: 'workspace-2', name: 'Codex' });
1081
- expect(first.created).toBe(true);
1082
- expect(second.created).toBe(false);
1083
- expect(second.profile.id).toBe(first.profile.id);
1084
- expect(otherWorkspace.created).toBe(true);
1085
- expect(otherWorkspace.profile.id).not.toBe(first.profile.id);
1086
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1087
- expect(core.listAiProfiles('workspace-2')).toHaveLength(1);
1088
- });
1089
- it('allows token-based re-resolve even when the ai profile limit is full', () => {
1090
- const projectRoot = createProjectRoot();
1091
- createdRoots.push(projectRoot);
1092
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1093
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1094
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
1095
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' });
1096
- const second = core.resolveAiProfile({
1097
- workspaceId: 'workspace-1',
1098
- name: 'Codex Prime',
1099
- icon: 'Sparkles',
1100
- profileToken: first.profile.profileToken
1101
- });
1102
- expect(second.created).toBe(false);
1103
- expect(second.profile.id).toBe(first.profile.id);
1104
- expect(second.profile.name).toBe('Codex Prime');
1105
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1106
- });
1107
- it('reuses an exact-match ai profile even when the ai profile limit is full', () => {
1108
- const projectRoot = createProjectRoot();
1109
- createdRoots.push(projectRoot);
1110
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1111
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1112
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
1113
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Automation Codex' }, { trustedSeatScope: 'cloud_metered' });
1114
- const second = core.resolveAiProfile({
1115
- workspaceId: 'workspace-1',
1116
- name: 'Automation Codex',
1117
- role: 'Assistant',
1118
- }, { trustedSeatScope: 'cloud_metered' });
1119
- expect(second.created).toBe(false);
1120
- expect(second.profile.id).toBe(first.profile.id);
1121
- expect(second.profile.role).toBe('Assistant');
1122
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1123
- });
1124
- it('blocks net-new ai profile creation when the ai profile limit is full', () => {
1125
- const projectRoot = createProjectRoot();
1126
- createdRoots.push(projectRoot);
1127
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1128
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1129
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
1130
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' }, { trustedSeatScope: 'cloud_metered' });
1131
- expect(first.created).toBe(true);
1132
- try {
1133
- core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude' }, { trustedSeatScope: 'cloud_metered' });
1134
- throw new Error('Expected AI profile creation to be blocked');
1135
- }
1136
- catch (error) {
1137
- expect(error.message).toContain('No AI seats are available for this workspace');
1138
- expect(error.code).toBe('AI_PROFILE_LIMIT_REACHED');
1139
- expect(error.statusCode).toBe(403);
1140
- expect(error.details).toEqual({
1141
- workspaceId: 'workspace-1',
1142
- limit: 1,
1143
- used: 1,
1144
- remaining: 0
1145
- });
1146
- }
1147
- });
1148
- it('counts only cloud-metered ai profiles toward the seat limit', () => {
1149
- const projectRoot = createProjectRoot();
1150
- createdRoots.push(projectRoot);
1151
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1152
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1153
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
1154
- const local = core.resolveAiProfile({
1155
- workspaceId: 'workspace-1',
1156
- name: 'Local Codex',
1157
- }, { trustedSeatScope: 'local_unmetered' });
1158
- const cloud = core.resolveAiProfile({
1159
- workspaceId: 'workspace-1',
1160
- name: 'Cloud Codex',
1161
- }, { trustedSeatScope: 'cloud_metered' });
1162
- const extraLocal = core.resolveAiProfile({
1163
- workspaceId: 'workspace-1',
1164
- name: 'Another Local Agent',
1165
- }, { trustedSeatScope: 'local_unmetered' });
1166
- expect(local.profile.seatScope).toBe('local_unmetered');
1167
- expect(cloud.profile.seatScope).toBe('cloud_metered');
1168
- expect(extraLocal.profile.seatScope).toBe('local_unmetered');
1169
- expect(core.countAiProfileUsage('workspace-1')).toEqual({
1170
- used: 1,
1171
- limit: 1,
1172
- remaining: 0
1173
- });
1174
- expect(core.listAiProfiles('workspace-1')).toHaveLength(3);
1175
- });
1176
- it('honors configured ai profile limits even when legacy plan data stores the feature as enabled', () => {
1177
- const projectRoot = createProjectRoot();
1178
- createdRoots.push(projectRoot);
1179
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1180
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1181
- core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
1182
- core.ensureBootstrapUserAccess({
1183
- userId: 'owner-u1',
1184
- workspaceId: 'workspace-1',
1185
- role: 'owner',
1186
- email: 'owner-u1@example.com'
1187
- });
1188
- core.updatePlanVersionFeatures({
1189
- planVersionId: 'free-v1',
1190
- features: [
1191
- { featureKey: 'collaboration.ai_profiles', access: 'enabled', config: { maxAiProfiles: 20 } }
1192
- ]
1193
- });
1194
- core.db.prepare(`
1195
- UPDATE plan_feature_matrix
1196
- SET access = 'enabled',
1197
- config_json = ?
1198
- WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
1199
- `).run(JSON.stringify({ maxAiProfiles: 20 }), 'default', 'free-v1', 'collaboration.ai_profiles');
1200
- core.updateAccountEntitlement({
1201
- accountId: 'owner-u1',
1202
- planVersionId: 'free-v1',
1203
- entitlementState: 'active',
1204
- actorUserId: 'system-admin',
1205
- actorRole: 'system_admin'
1206
- });
1207
- core.resolveAiProfile({
1208
- workspaceId: 'workspace-1',
1209
- name: 'Cloud Codex',
1210
- }, { trustedSeatScope: 'cloud_metered' });
1211
- expect(core.countAiProfileUsage('workspace-1')).toEqual({
1212
- used: 1,
1213
- limit: 20,
1214
- remaining: 19
1215
- });
1216
- });
1217
- it('blocks cloud promotion when a local ai profile would exceed the seat limit', () => {
1218
- const projectRoot = createProjectRoot();
1219
- createdRoots.push(projectRoot);
1220
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1221
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1222
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
1223
- core.resolveAiProfile({
1224
- workspaceId: 'workspace-1',
1225
- name: 'Cloud Bot',
1226
- }, { trustedSeatScope: 'cloud_metered' });
1227
- const local = core.resolveAiProfile({
1228
- workspaceId: 'workspace-1',
1229
- name: 'Hybrid Bot',
1230
- }, { trustedSeatScope: 'local_unmetered' });
1231
- try {
1232
- core.resolveAiProfile({
1233
- workspaceId: 'workspace-1',
1234
- name: 'Hybrid Bot',
1235
- }, { trustedSeatScope: 'cloud_metered' });
1236
- throw new Error('Expected cloud promotion to be blocked');
1237
- }
1238
- catch (error) {
1239
- expect(error.message).toContain('No AI seats are available for this workspace');
1240
- expect(error.code).toBe('AI_PROFILE_LIMIT_REACHED');
1241
- }
1242
- const refreshed = core.getAiProfileByToken({
1243
- workspaceId: 'workspace-1',
1244
- profileToken: local.profile.profileToken
1245
- });
1246
- expect(refreshed?.seatScope).toBe('local_unmetered');
1247
- });
1248
- it('keeps local and cloud MCP profiles separate when they share the same name', () => {
1249
- const projectRoot = createProjectRoot();
1250
- createdRoots.push(projectRoot);
1251
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1252
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1253
- const cloud = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
1254
- const local = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
1255
- expect(local.created).toBe(true);
1256
- expect(local.profile.id).not.toBe(cloud.profile.id);
1257
- expect(local.profile.seatScope).toBe('local_unmetered');
1258
- expect(cloud.profile.seatScope).toBe('cloud_metered');
1259
- expect(core.countAiProfileUsage('workspace-1')).toEqual({
1260
- used: 1,
1261
- limit: null,
1262
- remaining: null
1263
- });
1264
- expect(core.listAiProfiles('workspace-1')).toHaveLength(2);
1265
- });
1266
- it('claims a legacy unscoped profile for the trusted runtime instead of creating a duplicate', () => {
1267
- const projectRoot = createProjectRoot();
1268
- createdRoots.push(projectRoot);
1269
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1270
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1271
- const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex' });
1272
- expect(legacy.profile.seatScope ?? null).toBeNull();
1273
- const claimed = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
1274
- expect(claimed.created).toBe(false);
1275
- expect(claimed.profile.id).toBe(legacy.profile.id);
1276
- expect(claimed.profile.seatScope).toBe('local_unmetered');
1277
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1278
- });
1279
- it('reactivates a retired legacy unscoped profile for the trusted runtime instead of recreating it', () => {
1280
- const projectRoot = createProjectRoot();
1281
- createdRoots.push(projectRoot);
1282
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1283
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1284
- const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex' });
1285
- expect(legacy.profile.seatScope ?? null).toBeNull();
1286
- core.archiveAiProfile({
1287
- workspaceId: 'workspace-1',
1288
- profileId: legacy.profile.id,
1289
- archivedBy: 'owner-1',
1290
- reason: 'manual_archive'
1291
- });
1292
- const reactivated = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
1293
- expect(reactivated.created).toBe(false);
1294
- expect(reactivated.profile.id).toBe(legacy.profile.id);
1295
- expect(reactivated.profile.archivedAt).toBeNull();
1296
- expect(reactivated.profile.seatScope).toBe('cloud_metered');
1297
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1298
- });
1299
- it('merges two duplicate profiles, reassigning task assignees and comment authors', () => {
1300
- const projectRoot = createProjectRoot();
1301
- createdRoots.push(projectRoot);
1302
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1303
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1304
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity' });
1305
- const second = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity Clone' });
1306
- // Create a task assigned to the duplicate and a comment authored by it
1307
- const task = core.createTask({
1308
- title: 'Task with duplicate assignee',
1309
- assignee: second.profile.id,
1310
- comments: [{
1311
- id: 'c1',
1312
- author: second.profile.id,
1313
- text: 'Comment from duplicate',
1314
- timestamp: '2026-03-22T10:00:00.000Z'
1315
- }]
1316
- }, { workspaceId: 'workspace-1' });
1317
- // Merge the duplicate into the original
1318
- const merged = core.mergeAiProfiles({
1319
- workspaceId: 'workspace-1',
1320
- keepId: first.profile.id,
1321
- mergeId: second.profile.id
1322
- });
1323
- expect(merged.id).toBe(first.profile.id);
1324
- // Duplicate profile should leave the active roster but remain archived for history and sync.
1325
- const profiles = core.listAiProfiles('workspace-1');
1326
- expect(profiles.some((p) => p.id === second.profile.id)).toBe(false);
1327
- expect(profiles.some((p) => p.id === first.profile.id)).toBe(true);
1328
- const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
1329
- const archivedDuplicate = archivedProfiles.find((p) => p.id === second.profile.id);
1330
- expect(archivedDuplicate).toMatchObject({
1331
- id: second.profile.id,
1332
- archivedReason: 'merged_duplicate',
1333
- mergedIntoProfileId: first.profile.id,
1334
- archivedBy: first.profile.id
1335
- });
1336
- expect(archivedDuplicate?.archivedAt).toBeTruthy();
1337
- // Task assignee should point to the kept profile
1338
- const updatedTask = core.getTask(task.id, 'workspace-1');
1339
- expect(updatedTask?.assignee).toBe(first.profile.id);
1340
- // Comment author should point to the kept profile
1341
- expect(updatedTask?.comments?.[0]?.author).toBe(first.profile.id);
1342
- });
1343
- it('does not merge separate local and cloud AI profile registrations', () => {
1344
- const projectRoot = createProjectRoot();
1345
- createdRoots.push(projectRoot);
1346
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1347
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1348
- const cloud = core.resolveAiProfile({
1349
- workspaceId: 'workspace-1',
1350
- name: 'Codex',
1351
- surfaceType: 'coding_tool'
1352
- }, { trustedSeatScope: 'cloud_metered' });
1353
- const local = core.resolveAiProfile({
1354
- workspaceId: 'workspace-1',
1355
- name: 'Codex',
1356
- surfaceType: 'coding_tool'
1357
- }, { trustedSeatScope: 'local_unmetered' });
1358
- expect(() => core.mergeAiProfiles({
1359
- workspaceId: 'workspace-1',
1360
- keepId: local.profile.id,
1361
- mergeId: cloud.profile.id
1362
- })).toThrow(/separate registrations/);
1363
- expect(core.listAiProfiles('workspace-1').map((profile) => profile.id).sort()).toEqual([cloud.profile.id, local.profile.id].sort());
1364
- });
1365
- it('archives AI profiles out of the active roster while preserving history and freeing seats', () => {
1366
- const projectRoot = createProjectRoot();
1367
- createdRoots.push(projectRoot);
1368
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1369
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1370
- enableAiProfileSeatLimit(core, 'workspace-1', 'owner-1', 1);
1371
- const first = core.resolveAiProfile({
1372
- workspaceId: 'workspace-1',
1373
- name: 'Codex',
1374
- surfaceType: 'coding_tool'
1375
- });
1376
- const archived = core.archiveAiProfile({
1377
- workspaceId: 'workspace-1',
1378
- profileId: first.profile.id,
1379
- archivedBy: 'owner-1',
1380
- reason: 'manual_archive'
1381
- });
1382
- expect(archived.id).toBe(first.profile.id);
1383
- expect(archived.archivedReason).toBe('manual_archive');
1384
- expect(archived.archivedBy).toBe('owner-1');
1385
- expect(archived.archivedAt).toBeTruthy();
1386
- expect(core.listAiProfiles('workspace-1')).toEqual([]);
1387
- expect(core.listAssignableActors('workspace-1').some((actor) => actor.id === first.profile.id)).toBe(false);
1388
- const historicalActor = core.resolveWorkspaceActor('workspace-1', first.profile.id);
1389
- expect(historicalActor?.id).toBe(first.profile.id);
1390
- expect(historicalActor?.label).toBe('Codex');
1391
- expect(historicalActor?.meta).toEqual(expect.objectContaining({
1392
- archivedReason: 'manual_archive'
1393
- }));
1394
- const second = core.resolveAiProfile({
1395
- workspaceId: 'workspace-1',
1396
- name: 'Claude',
1397
- surfaceType: 'coding_tool'
1398
- });
1399
- expect(second.created).toBe(true);
1400
- const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
1401
- expect(archivedProfiles.map((profile) => profile.name)).toEqual(['Claude', 'Codex']);
1402
- const archivedEntry = archivedProfiles.find((profile) => profile.id === first.profile.id);
1403
- expect(archivedEntry?.archivedReason).toBe('manual_archive');
1404
- expect(archivedEntry?.rosterStateUpdatedAt).toBe(archivedEntry?.archivedAt);
1405
- });
1406
- it('keeps an archived AI profile archived when sync replays an equal-watermark active copy', () => {
1407
- const projectRoot = createProjectRoot();
1408
- createdRoots.push(projectRoot);
1409
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1410
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1411
- const resolved = core.resolveAiProfile({
1412
- workspaceId: 'workspace-1',
1413
- name: 'Codex T-714 Verifier',
1414
- surfaceType: 'coding_tool'
1415
- });
1416
- const archived = core.archiveAiProfile({
1417
- workspaceId: 'workspace-1',
1418
- profileId: resolved.profile.id,
1419
- archivedBy: 'owner-1',
1420
- reason: 'manual_archive'
1421
- });
1422
- const replayedActive = core.upsertAiProfileForSync({
1423
- ...archived,
1424
- archivedAt: null,
1425
- archivedBy: null,
1426
- archivedReason: null,
1427
- mergedIntoProfileId: null,
1428
- updatedAt: archived.updatedAt
1429
- });
1430
- expect(replayedActive.archivedAt).toBeTruthy();
1431
- expect(core.listAiProfiles('workspace-1')).toEqual([]);
1432
- const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
1433
- .find((profile) => profile.id === archived.id);
1434
- expect(stored?.archivedAt).toBe(archived.archivedAt);
1435
- expect(stored?.archivedReason).toBe('manual_archive');
1436
- });
1437
- it('accepts newer synced seat scope for the same AI profile id', () => {
1438
- const projectRoot = createProjectRoot();
1439
- createdRoots.push(projectRoot);
1440
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1441
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1442
- const contaminated = core.resolveAiProfile({
1443
- workspaceId: 'workspace-1',
1444
- name: 'Codex',
1445
- surfaceType: 'coding_tool'
1446
- }, { trustedSeatScope: 'cloud_metered' });
1447
- const repairedAt = new Date(Date.parse(contaminated.profile.updatedAt) + 1000).toISOString();
1448
- const repaired = core.upsertAiProfileForSync({
1449
- ...contaminated.profile,
1450
- seatScope: 'local_unmetered',
1451
- updatedAt: repairedAt,
1452
- lastActiveAt: repairedAt
1453
- });
1454
- expect(repaired.id).toBe(contaminated.profile.id);
1455
- expect(repaired.seatScope).toBe('local_unmetered');
1456
- const staleReplay = core.upsertAiProfileForSync({
1457
- ...contaminated.profile,
1458
- seatScope: 'cloud_metered',
1459
- updatedAt: contaminated.profile.updatedAt
1460
- });
1461
- expect(staleReplay.seatScope).toBe('local_unmetered');
1462
- });
1463
- it('keeps an archived AI profile archived when sync replays a newer metadata update with an older lifecycle timestamp', () => {
1464
- const projectRoot = createProjectRoot();
1465
- createdRoots.push(projectRoot);
1466
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1467
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1468
- const resolved = core.resolveAiProfile({
1469
- workspaceId: 'workspace-1',
1470
- name: 'Antigravity/jCodeMunch',
1471
- surfaceType: 'coding_tool'
1472
- });
1473
- const archived = core.archiveAiProfile({
1474
- workspaceId: 'workspace-1',
1475
- profileId: resolved.profile.id,
1476
- archivedBy: 'owner-1',
1477
- reason: 'manual_archive'
1478
- });
1479
- const replayedActive = core.upsertAiProfileForSync({
1480
- ...archived,
1481
- description: 'Cloud-side metadata changed after the archive write was missed.',
1482
- archivedAt: null,
1483
- archivedBy: null,
1484
- archivedReason: null,
1485
- mergedIntoProfileId: null,
1486
- rosterStateUpdatedAt: resolved.profile.rosterStateUpdatedAt,
1487
- updatedAt: '2099-03-22T10:30:00.000Z'
1488
- });
1489
- expect(replayedActive.archivedAt).toBeTruthy();
1490
- const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
1491
- .find((profile) => profile.id === archived.id);
1492
- expect(stored?.archivedAt).toBe(archived.archivedAt);
1493
- expect(stored?.description).toBe('Cloud-side metadata changed after the archive write was missed.');
1494
- });
1495
- it('repairs stale archived roster lifecycle timestamps before applying active sync snapshots', () => {
1496
- const projectRoot = createProjectRoot();
1497
- createdRoots.push(projectRoot);
1498
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1499
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1500
- const resolved = core.resolveAiProfile({
1501
- workspaceId: 'workspace-1',
1502
- name: 'Codex',
1503
- surfaceType: 'coding_tool'
1504
- });
1505
- const archived = core.archiveAiProfile({
1506
- workspaceId: 'workspace-1',
1507
- profileId: resolved.profile.id,
1508
- archivedBy: 'owner-1',
1509
- reason: 'manual_archive'
1510
- });
1511
- core.db.prepare(`
1512
- UPDATE ai_profiles
1513
- SET roster_state_updated_at = ?
1514
- WHERE tenant_id = ? AND id = ?
1515
- `).run(resolved.profile.createdAt, 'default', archived.id);
1516
- const replayedActive = core.upsertAiProfileForSync({
1517
- ...archived,
1518
- description: 'Cloud active snapshot with newer metadata but no lifecycle marker.',
1519
- archivedAt: null,
1520
- archivedBy: null,
1521
- archivedReason: null,
1522
- mergedIntoProfileId: null,
1523
- rosterStateUpdatedAt: null,
1524
- updatedAt: '2099-03-22T10:30:00.000Z'
1525
- });
1526
- expect(replayedActive.archivedAt).toBe(archived.archivedAt);
1527
- expect(replayedActive.rosterStateUpdatedAt).toBe(archived.archivedAt);
1528
- const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
1529
- .find((profile) => profile.id === archived.id);
1530
- expect(stored?.archivedAt).toBe(archived.archivedAt);
1531
- expect(stored?.description).toBe('Cloud active snapshot with newer metadata but no lifecycle marker.');
1532
- expect(core.listAiProfiles('workspace-1')).toEqual([]);
1533
- });
1534
- it('allows sync to reactivate an archived AI profile when lifecycle timestamp is newer', () => {
1535
- const projectRoot = createProjectRoot();
1536
- createdRoots.push(projectRoot);
1537
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1538
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1539
- const resolved = core.resolveAiProfile({
1540
- workspaceId: 'workspace-1',
1541
- name: 'Claude Code',
1542
- surfaceType: 'coding_tool'
1543
- });
1544
- const archived = core.archiveAiProfile({
1545
- workspaceId: 'workspace-1',
1546
- profileId: resolved.profile.id,
1547
- archivedBy: 'owner-1',
1548
- reason: 'manual_archive'
1549
- });
1550
- const reactivated = core.upsertAiProfileForSync({
1551
- ...archived,
1552
- archivedAt: null,
1553
- archivedBy: null,
1554
- archivedReason: null,
1555
- mergedIntoProfileId: null,
1556
- rosterStateUpdatedAt: '2099-03-22T10:45:00.000Z',
1557
- updatedAt: '2099-03-22T10:45:00.000Z'
1558
- });
1559
- expect(reactivated.archivedAt).toBeNull();
1560
- expect(core.listAiProfiles('workspace-1').map((profile) => profile.id)).toEqual([resolved.profile.id]);
1561
- });
1562
- it('reactivates an archived AI profile when resolveAiProfile is called explicitly', () => {
1563
- const projectRoot = createProjectRoot();
1564
- createdRoots.push(projectRoot);
1565
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1566
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1567
- const initial = core.resolveAiProfile({
1568
- workspaceId: 'workspace-1',
1569
- name: 'Codex',
1570
- surfaceType: 'coding_tool',
1571
- }, { trustedSeatScope: 'cloud_metered' });
1572
- core.archiveAiProfile({
1573
- workspaceId: 'workspace-1',
1574
- profileId: initial.profile.id,
1575
- archivedBy: 'owner-1',
1576
- reason: 'manual_archive'
1577
- });
1578
- const reactivatedByName = core.resolveAiProfile({
1579
- workspaceId: 'workspace-1',
1580
- name: 'Codex',
1581
- surfaceType: 'coding_tool'
1582
- }, { trustedSeatScope: 'cloud_metered' });
1583
- expect(reactivatedByName.created).toBe(false);
1584
- expect(reactivatedByName.profile.id).toBe(initial.profile.id);
1585
- expect(reactivatedByName.profile.archivedAt).toBeNull();
1586
- expect(reactivatedByName.profile.archivedReason).toBeNull();
1587
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1588
- core.archiveAiProfile({
1589
- workspaceId: 'workspace-1',
1590
- profileId: initial.profile.id,
1591
- archivedBy: 'owner-1',
1592
- reason: 'manual_archive'
1593
- });
1594
- const reactivatedByToken = core.resolveAiProfile({
1595
- workspaceId: 'workspace-1',
1596
- name: 'Codex',
1597
- profileToken: initial.profile.profileToken,
1598
- surfaceType: 'coding_tool'
1599
- }, { trustedSeatScope: 'cloud_metered' });
1600
- expect(reactivatedByToken.created).toBe(false);
1601
- expect(reactivatedByToken.profile.id).toBe(initial.profile.id);
1602
- expect(reactivatedByToken.profile.profileToken).toBe(initial.profile.profileToken);
1603
- expect(reactivatedByToken.profile.archivedAt).toBeNull();
1604
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1605
- });
1606
- it('rejects archiving an AI profile that still owns open tasks', () => {
1607
- const projectRoot = createProjectRoot();
1608
- createdRoots.push(projectRoot);
1609
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1610
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1611
- const profile = core.resolveAiProfile({
1612
- workspaceId: 'workspace-1',
1613
- name: 'Antigravity',
1614
- surfaceType: 'agent'
1615
- });
1616
- core.createTask({
1617
- title: 'Still assigned',
1618
- status: 'in-progress',
1619
- assignee: profile.profile.id
1620
- }, { workspaceId: 'workspace-1' });
1621
- try {
1622
- core.archiveAiProfile({
1623
- workspaceId: 'workspace-1',
1624
- profileId: profile.profile.id,
1625
- archivedBy: 'owner-1',
1626
- reason: 'manual_archive'
1627
- });
1628
- expect.unreachable('Expected archiveAiProfile to reject open-task ownership');
1629
- }
1630
- catch (error) {
1631
- expect(error.code).toBe('AI_PROFILE_HAS_OPEN_TASKS');
1632
- expect(error.statusCode).toBe(409);
1633
- expect(error.details).toEqual({
1634
- workspaceId: 'workspace-1',
1635
- profileId: profile.profile.id,
1636
- openTaskCount: 1
1637
- });
1638
- }
1639
- expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1640
- expect(core.listAiProfiles('workspace-1', { includeArchived: true })[0]?.archivedAt).toBeNull();
1641
- });
1642
- it('canonicalizes AI names to profile ids when creating tasks and comments', () => {
1643
- const projectRoot = createProjectRoot();
1644
- createdRoots.push(projectRoot);
1645
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1646
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1647
- const resolved = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'ChatGPT', color: '#10a37f' });
1648
- const task = core.createTask({
1649
- title: 'Canonicalize actor refs',
1650
- createdBy: 'chatgpt',
1651
- assignee: 'ChatGPT',
1652
- comments: [{
1653
- id: 'c1',
1654
- author: 'chatgpt',
1655
- text: 'Stored as canonical profile id',
1656
- timestamp: '2026-03-22T10:00:00.000Z'
1657
- }]
1658
- }, { workspaceId: 'workspace-1' });
1659
- const fetched = core.getTask(task.id, 'workspace-1');
1660
- expect(fetched?.createdBy).toBe(resolved.profile.id);
1661
- expect(fetched?.assignee).toBe(resolved.profile.id);
1662
- expect(fetched?.comments?.[0]?.author).toBe(resolved.profile.id);
1663
- expect(fetched?.comments?.[0]?.submittedAuthor).toBe('chatgpt');
1664
- expect(fetched?.assigneeActor?.label).toBe('ChatGPT');
1665
- expect(fetched?.comments?.[0]?.actor?.label).toBe('ChatGPT');
1666
- });
1667
- it('keeps ambiguous legacy AI comment aliases unresolved instead of rebinding them', () => {
1668
- const projectRoot = createProjectRoot();
1669
- createdRoots.push(projectRoot);
1670
- const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1671
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1672
- const cloud = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
1673
- const local = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
1674
- const task = core.createTask({
1675
- title: 'Ambiguous legacy comment alias',
1676
- comments: [{
1677
- id: 'legacy-codex',
1678
- author: 'codex',
1679
- text: 'Leave me unresolved',
1680
- timestamp: '2026-03-22T10:00:00.000Z'
1681
- }]
1682
- }, { workspaceId: 'workspace-1' });
1683
- const fetched = core.getTask(task.id, 'workspace-1');
1684
- expect(fetched?.comments?.[0]?.author).toBe('codex');
1685
- expect(fetched?.comments?.[0]?.actor).toEqual(expect.objectContaining({
1686
- id: 'codex',
1687
- kind: 'ai',
1688
- label: 'Codex',
1689
- meta: expect.objectContaining({ unresolved: true })
1690
- }));
1691
- expect(fetched?.comments?.[0]?.actor?.id).not.toBe(cloud.profile.id);
1692
- expect(fetched?.comments?.[0]?.actor?.id).not.toBe(local.profile.id);
1693
- });
1694
- it('repairs legacy comments from submittedAuthor under the v2 migration even after v1 completed', () => {
1695
- const projectRoot = createProjectRoot();
1696
- createdRoots.push(projectRoot);
1697
- const firstCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1698
- firstCore.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1699
- const resolved = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude', color: '#f59e0b' });
1700
- const task = firstCore.createTask({
1701
- title: 'Repair legacy submitted author',
1702
- comments: [{
1703
- id: 'legacy-agent',
1704
- author: 'agent',
1705
- submittedAuthor: 'Claude',
1706
- text: 'Legacy comment should repair forward',
1707
- timestamp: '2026-03-22T10:00:00.000Z'
1708
- }]
1709
- }, { workspaceId: 'workspace-1' });
1710
- const firstDb = firstCore.db;
1711
- const originalTaskWatermark = '2026-03-22T10:00:00.000Z';
1712
- firstDb.prepare(`
1713
- UPDATE tasks
1714
- SET updated_at = ?
1715
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1716
- `).run(originalTaskWatermark, 'default', 'workspace-1', task.id);
1717
- const completedAt = '2026-03-22T11:00:00.000Z';
1718
- firstDb.prepare(`
1719
- INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
1720
- VALUES (?, ?, ?, ?)
1721
- ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
1722
- payload = excluded.payload,
1723
- updated_at = excluded.updated_at
1724
- `).run('default', 'migration.actor_refs.canonicalize.v1', JSON.stringify({ completedAt }), completedAt);
1725
- firstDb.prepare(`
1726
- DELETE FROM system_settings
1727
- WHERE tenant_id = ? AND settings_key = ?
1728
- `).run('default', 'migration.actor_refs.canonicalize_comments.v2');
1729
- firstCore.close();
1730
- const secondCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1731
- const fetched = secondCore.getTask(task.id, 'workspace-1');
1732
- expect(fetched?.comments?.[0]?.author).toBe(resolved.profile.id);
1733
- expect(fetched?.comments?.[0]?.submittedAuthor).toBe('Claude');
1734
- expect(fetched?.comments?.[0]?.actor?.id).toBe(resolved.profile.id);
1735
- expect(Date.parse(String(fetched?.updatedAt || ''))).toBeGreaterThan(Date.parse(originalTaskWatermark));
1736
- const syncRows = secondCore.listTasksForSync('workspace-1', {
1737
- watermark: originalTaskWatermark,
1738
- id: task.id,
1739
- }, 10);
1740
- expect(syncRows).toHaveLength(1);
1741
- expect(syncRows[0]?.task?.id).toBe(task.id);
1742
- expect(syncRows[0]?.task?.comments?.[0]?.author).toBe(resolved.profile.id);
1743
- const secondDb = secondCore.db;
1744
- const migrationRow = secondDb.prepare(`
1745
- SELECT payload
1746
- FROM system_settings
1747
- WHERE tenant_id = ? AND settings_key = ?
1748
- LIMIT 1
1749
- `).get('default', 'migration.actor_refs.canonicalize_comments.v2');
1750
- expect(JSON.parse(String(migrationRow?.payload || '{}'))).toEqual(expect.objectContaining({
1751
- repairedCount: 1,
1752
- ambiguousCount: 0,
1753
- skippedCount: expect.any(Number)
1754
- }));
1755
- secondCore.close();
1756
- });
1757
- it('keeps repaired legacy comment sync watermarks monotonic when a task watermark is already ahead of local now', () => {
1758
- const projectRoot = createProjectRoot();
1759
- createdRoots.push(projectRoot);
1760
- const firstCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1761
- firstCore.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1762
- const resolved = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude', color: '#f59e0b' });
1763
- const task = firstCore.createTask({
1764
- title: 'Repair legacy submitted author with future watermark',
1765
- comments: [{
1766
- id: 'legacy-agent-future',
1767
- author: 'agent',
1768
- submittedAuthor: 'Claude',
1769
- text: 'Legacy comment should repair without moving sync backwards',
1770
- timestamp: '2026-03-22T10:00:00.000Z'
1771
- }]
1772
- }, { workspaceId: 'workspace-1' });
1773
- const firstDb = firstCore.db;
1774
- const originalTaskWatermark = '2099-03-22T10:00:00.000Z';
1775
- firstDb.prepare(`
1776
- UPDATE tasks
1777
- SET updated_at = ?
1778
- WHERE tenant_id = ? AND workspace_id = ? AND id = ?
1779
- `).run(originalTaskWatermark, 'default', 'workspace-1', task.id);
1780
- const completedAt = '2026-03-22T11:00:00.000Z';
1781
- firstDb.prepare(`
1782
- INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
1783
- VALUES (?, ?, ?, ?)
1784
- ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
1785
- payload = excluded.payload,
1786
- updated_at = excluded.updated_at
1787
- `).run('default', 'migration.actor_refs.canonicalize.v1', JSON.stringify({ completedAt }), completedAt);
1788
- firstDb.prepare(`
1789
- DELETE FROM system_settings
1790
- WHERE tenant_id = ? AND settings_key = ?
1791
- `).run('default', 'migration.actor_refs.canonicalize_comments.v2');
1792
- firstCore.close();
1793
- const secondCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1794
- const fetched = secondCore.getTask(task.id, 'workspace-1');
1795
- expect(fetched?.comments?.[0]?.author).toBe(resolved.profile.id);
1796
- expect(Date.parse(String(fetched?.updatedAt || ''))).toBeGreaterThan(Date.parse(originalTaskWatermark));
1797
- const syncRows = secondCore.listTasksForSync('workspace-1', {
1798
- watermark: originalTaskWatermark,
1799
- id: task.id,
1800
- }, 10);
1801
- expect(syncRows).toHaveLength(1);
1802
- expect(syncRows[0]?.task?.id).toBe(task.id);
1803
- expect(syncRows[0]?.task?.comments?.[0]?.author).toBe(resolved.profile.id);
1804
- secondCore.close();
1805
- });
1806
- it('repairs legacy actor refs and merges duplicate ai profiles on startup', () => {
1807
- const projectRoot = createProjectRoot();
1808
- createdRoots.push(projectRoot);
1809
- const firstCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1810
- firstCore.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1811
- const firstDb = firstCore.db;
1812
- const first = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity' });
1813
- const second = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Antigravity Clone' });
1814
- firstDb.prepare(`UPDATE ai_profiles SET name = 'Antigravity', username = 'antigravity' WHERE tenant_id = ? AND id = ?`)
1815
- .run('default', second.profile.id);
1816
- firstDb.prepare(`DELETE FROM system_settings WHERE tenant_id = ? AND settings_key IN (?, ?)`)
1817
- .run('default', 'migration.ai_profiles.dedupe.v1', 'migration.actor_refs.canonicalize.v1');
1818
- const task = firstCore.createTask({
1819
- title: 'Legacy actor refs',
1820
- createdBy: 'claude',
1821
- assignee: 'antigravity',
1822
- comments: [
1823
- {
1824
- id: 'legacy-comment',
1825
- author: 'claude',
1826
- text: 'Legacy claude alias',
1827
- timestamp: '2026-03-22T10:00:00.000Z'
1828
- }
1829
- ]
1830
- }, { workspaceId: 'workspace-1' });
1831
- const claude = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude' });
1832
- expect(task.comments?.[0]?.author).toBe('claude');
1833
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1834
- const reloaded = reloadedCore.getTask(task.id, 'workspace-1');
1835
- const profiles = reloadedCore.listAiProfiles('workspace-1');
1836
- expect(profiles.map((profile) => profile.name)).toEqual(['Antigravity', 'Claude']);
1837
- const survivingAntigravityId = profiles.find((profile) => profile.name === 'Antigravity')?.id;
1838
- expect(reloaded?.createdBy).toBe(claude.profile.id);
1839
- expect(reloaded?.assignee).toBe(survivingAntigravityId);
1840
- expect(reloaded?.comments?.[0]?.author).toBe(claude.profile.id);
1841
- expect(reloaded?.comments?.[0]?.actor?.label).toBe('Claude');
1842
- });
1843
- it('does not repair ambiguous legacy comment authors on startup', () => {
1844
- const projectRoot = createProjectRoot();
1845
- createdRoots.push(projectRoot);
1846
- const firstCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1847
- firstCore.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1848
- const firstDb = firstCore.db;
1849
- const first = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude', color: '#f59e0b' });
1850
- const second = firstCore.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude 2', color: '#0ea5e9' });
1851
- const task = firstCore.createTask({
1852
- title: 'Ambiguous legacy actor refs',
1853
- comments: [{
1854
- id: 'legacy-comment',
1855
- author: first.profile.id,
1856
- text: 'Leave ambiguous alias untouched.',
1857
- timestamp: '2026-03-22T10:00:00.000Z'
1858
- }]
1859
- }, { workspaceId: 'workspace-1' });
1860
- firstDb.prepare(`UPDATE ai_profiles SET name = 'Claude', username = 'claude' WHERE tenant_id = ? AND id = ?`)
1861
- .run('default', second.profile.id);
1862
- firstDb.prepare(`UPDATE comments SET author = 'claude', submitted_author = 'claude' WHERE tenant_id = ? AND task_id = ? AND id = ?`)
1863
- .run('default', task.id, 'legacy-comment');
1864
- firstDb.prepare(`DELETE FROM system_settings WHERE tenant_id = ? AND settings_key = ?`)
1865
- .run('default', 'migration.actor_refs.canonicalize.v1');
1866
- const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1867
- const reloaded = reloadedCore.getTask(task.id, 'workspace-1');
1868
- expect(reloaded?.comments?.[0]?.author).toBe('claude');
1869
- expect(reloaded?.comments?.[0]?.submittedAuthor).toBe('claude');
1870
- expect(reloaded?.comments?.[0]?.actor?.meta?.unresolved).toBe(true);
1871
- expect(reloaded?.comments?.[0]?.actor?.label).toBe('Claude');
1872
- });
1873
- });