@taskforcehq/taskforce 0.3.311 → 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 (454) hide show
  1. package/README.md +40 -2
  2. package/dist/TaskforceCore.js +1 -1
  3. package/dist/components/features/InitiativesModule.js +1 -1
  4. package/dist/components/features/TaskSettings.js +1 -1
  5. package/dist/components/views/StandaloneLayout.js +3 -6
  6. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +1 -2
  7. package/dist/components/views/standalone/modals/AccountHubModal.js +2 -3
  8. package/dist/components/views/standalone/modals/HelpModal.js +15 -2
  9. package/dist/hooks/useFilterSort.js +1 -1
  10. package/dist/hooks/useTaskforce.js +2 -2
  11. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +5 -2
  12. package/dist/ui/assets/{AgentsModule-D8u9P5dZ.js → AgentsModule-BULqIo8e.js} +1 -1
  13. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CsUauFA2.js → AnnotatedAttachmentWorkspace-DHr4rHK9.js} +1 -1
  14. package/dist/ui/assets/{ContextAttachmentManager-DbQvDHFz.js → ContextAttachmentManager-DoMe2OpY.js} +1 -1
  15. package/dist/ui/assets/{DocumentWorkspace-BmTkN1A8.js → DocumentWorkspace-DumBWhvb.js} +1 -1
  16. package/dist/ui/assets/{EntityActivityTimeline-BXHTM-Nh.js → EntityActivityTimeline-sHk8Nzbq.js} +1 -1
  17. package/dist/ui/assets/{InitiativesModule-D33RyXx0.js → InitiativesModule-3X9UNzHh.js} +1 -1
  18. package/dist/ui/assets/{PlansPage-D0RXlT9_.js → PlansPage-C3A3EYaV.js} +1 -1
  19. package/dist/ui/assets/{TaskContextUpload-Vsg67lpR.js → TaskContextUpload-C8G7pWkX.js} +1 -1
  20. package/dist/ui/assets/{TaskSettings-DshLw7_d.js → TaskSettings-DM1o9Wla.js} +2 -2
  21. package/dist/ui/assets/{WorkflowsModule-CLM-1BnF.js → WorkflowsModule-CW0crzvD.js} +1 -1
  22. package/dist/ui/assets/documentReferences-BZB3nMRb.js +1 -0
  23. package/dist/ui/assets/index-BWFQ9mGn.js +5 -0
  24. package/dist/ui/assets/{index-DOR9PRqt.css → index-DcK_8tuE.css} +1 -1
  25. package/dist/ui/index.html +2 -2
  26. package/package.json +12 -4
  27. package/scripts/postinstall.js +4 -3
  28. package/dist/TaskforceCore.CategoryFallback.test.d.ts +0 -1
  29. package/dist/TaskforceCore.CategoryFallback.test.js +0 -101
  30. package/dist/TaskforceCore.filter-persistence.test.d.ts +0 -1
  31. package/dist/TaskforceCore.filter-persistence.test.js +0 -402
  32. package/dist/TaskforceCore.routing.test.d.ts +0 -1
  33. package/dist/TaskforceCore.routing.test.js +0 -311
  34. package/dist/TaskforceCore.test.d.ts +0 -1
  35. package/dist/TaskforceCore.test.js +0 -7910
  36. package/dist/TaskforceWidget.test.d.ts +0 -1
  37. package/dist/TaskforceWidget.test.js +0 -92
  38. package/dist/UnsavedChangesRelaxed.test.d.ts +0 -1
  39. package/dist/UnsavedChangesRelaxed.test.js +0 -16
  40. package/dist/__tests__/localizationCatalogs.test.d.ts +0 -1
  41. package/dist/__tests__/localizationCatalogs.test.js +0 -54
  42. package/dist/__tests__/workflowSimulation.test.d.ts +0 -1
  43. package/dist/__tests__/workflowSimulation.test.js +0 -113
  44. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts +0 -1
  45. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +0 -278
  46. package/dist/annotatedAttachments/payload.test.d.ts +0 -1
  47. package/dist/annotatedAttachments/payload.test.js +0 -67
  48. package/dist/compat/workspaceSyncCompat.test.d.ts +0 -1
  49. package/dist/compat/workspaceSyncCompat.test.js +0 -124
  50. package/dist/components/features/AgentsModule.test.d.ts +0 -1
  51. package/dist/components/features/AgentsModule.test.js +0 -601
  52. package/dist/components/features/AnnotatedAttachmentWorkspace.test.d.ts +0 -1
  53. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +0 -2883
  54. package/dist/components/features/DocumentWorkspace.test.d.ts +0 -1
  55. package/dist/components/features/DocumentWorkspace.test.js +0 -1439
  56. package/dist/components/features/TaskSettings.test.d.ts +0 -1
  57. package/dist/components/features/TaskSettings.test.js +0 -1674
  58. package/dist/components/features/settings/TaxonomyLibraryManager.test.d.ts +0 -1
  59. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +0 -141
  60. package/dist/components/task/TaskActionHeader.test.d.ts +0 -1
  61. package/dist/components/task/TaskActionHeader.test.js +0 -91
  62. package/dist/components/task/TaskCard.test.d.ts +0 -1
  63. package/dist/components/task/TaskCard.test.js +0 -513
  64. package/dist/components/task/TaskContextUpload.test.d.ts +0 -1
  65. package/dist/components/task/TaskContextUpload.test.js +0 -506
  66. package/dist/components/task/TaskForm.test.d.ts +0 -1
  67. package/dist/components/task/TaskForm.test.js +0 -946
  68. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +0 -1
  69. package/dist/components/task/TaskKanban.empty-column-drop.test.js +0 -66
  70. package/dist/components/task/TaskKanban.planning-drop.test.d.ts +0 -1
  71. package/dist/components/task/TaskKanban.planning-drop.test.js +0 -93
  72. package/dist/components/task/TaskKanban.test.d.ts +0 -1
  73. package/dist/components/task/TaskKanban.test.js +0 -99
  74. package/dist/components/task/TaskList.test.d.ts +0 -1
  75. package/dist/components/task/TaskList.test.js +0 -473
  76. package/dist/components/task/TaskStatusActions.test.d.ts +0 -1
  77. package/dist/components/task/TaskStatusActions.test.js +0 -54
  78. package/dist/components/task/taskKanbanDropRules.test.d.ts +0 -1
  79. package/dist/components/task/taskKanbanDropRules.test.js +0 -114
  80. package/dist/components/ui/LevelSelector.test.d.ts +0 -1
  81. package/dist/components/ui/LevelSelector.test.js +0 -55
  82. package/dist/components/ui/Markdown.test.d.ts +0 -1
  83. package/dist/components/ui/Markdown.test.js +0 -38
  84. package/dist/components/ui/MultiSelectFilter.test.d.ts +0 -1
  85. package/dist/components/ui/MultiSelectFilter.test.js +0 -74
  86. package/dist/components/ui/TaxonomyDropdown.test.d.ts +0 -1
  87. package/dist/components/ui/TaxonomyDropdown.test.js +0 -130
  88. package/dist/components/ui/TopNoticeLayer.test.d.ts +0 -1
  89. package/dist/components/ui/TopNoticeLayer.test.js +0 -25
  90. package/dist/components/views/AppShellHeader.test.d.ts +0 -1
  91. package/dist/components/views/AppShellHeader.test.js +0 -42
  92. package/dist/components/views/PlanComparisonPage.test.d.ts +0 -1
  93. package/dist/components/views/PlanComparisonPage.test.js +0 -758
  94. package/dist/components/views/PlansPage.test.d.ts +0 -1
  95. package/dist/components/views/PlansPage.test.js +0 -710
  96. package/dist/components/views/chrome/WorkspaceChromeSections.test.d.ts +0 -1
  97. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +0 -54
  98. package/dist/components/views/chrome/workspaceChrome.test.d.ts +0 -1
  99. package/dist/components/views/chrome/workspaceChrome.test.js +0 -52
  100. package/dist/components/views/panels/DocumentFilterToolbar.test.d.ts +0 -1
  101. package/dist/components/views/panels/DocumentFilterToolbar.test.js +0 -19
  102. package/dist/components/views/panels/FilterToolbar.test.d.ts +0 -1
  103. package/dist/components/views/panels/FilterToolbar.test.js +0 -47
  104. package/dist/components/views/panels/PlanningDrawer.test.d.ts +0 -1
  105. package/dist/components/views/panels/PlanningDrawer.test.js +0 -529
  106. package/dist/components/views/planningScope.test.d.ts +0 -1
  107. package/dist/components/views/planningScope.test.js +0 -60
  108. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +0 -1
  109. package/dist/components/views/standalone/modals/AccountHubModal.test.js +0 -72
  110. package/dist/components/views/standalone/modals/HelpModal.test.d.ts +0 -1
  111. package/dist/components/views/standalone/modals/HelpModal.test.js +0 -23
  112. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +0 -1
  113. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +0 -24
  114. package/dist/components/views/teamManagementAccess.test.d.ts +0 -1
  115. package/dist/components/views/teamManagementAccess.test.js +0 -44
  116. package/dist/config/deployment.test.d.ts +0 -1
  117. package/dist/config/deployment.test.js +0 -90
  118. package/dist/config/envSchema.test.d.ts +0 -1
  119. package/dist/config/envSchema.test.js +0 -125
  120. package/dist/core/AiProfileService.test.d.ts +0 -1
  121. package/dist/core/AiProfileService.test.js +0 -184
  122. package/dist/core/AiProfiles.test.d.ts +0 -1
  123. package/dist/core/AiProfiles.test.js +0 -1873
  124. package/dist/core/AuthTokenService.test.d.ts +0 -1
  125. package/dist/core/AuthTokenService.test.js +0 -78
  126. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +0 -1
  127. package/dist/core/BulkUpdateFieldsPreview.test.js +0 -62
  128. package/dist/core/CompletedAt.test.d.ts +0 -1
  129. package/dist/core/CompletedAt.test.js +0 -44
  130. package/dist/core/CreatedBy.test.d.ts +0 -1
  131. package/dist/core/CreatedBy.test.js +0 -188
  132. package/dist/core/DeletedTaskTrash.test.d.ts +0 -1
  133. package/dist/core/DeletedTaskTrash.test.js +0 -104
  134. package/dist/core/EntitlementsPolicy.test.d.ts +0 -1
  135. package/dist/core/EntitlementsPolicy.test.js +0 -280
  136. package/dist/core/GlobalSettingsService.test.d.ts +0 -1
  137. package/dist/core/GlobalSettingsService.test.js +0 -45
  138. package/dist/core/JsonBackupRuntime.test.d.ts +0 -1
  139. package/dist/core/JsonBackupRuntime.test.js +0 -155
  140. package/dist/core/PlanFeatureCatalog.test.d.ts +0 -1
  141. package/dist/core/PlanFeatureCatalog.test.js +0 -139
  142. package/dist/core/PlanVersionPolicy.test.d.ts +0 -1
  143. package/dist/core/PlanVersionPolicy.test.js +0 -236
  144. package/dist/core/PlanningEntities.test.d.ts +0 -1
  145. package/dist/core/PlanningEntities.test.js +0 -509
  146. package/dist/core/ScheduleFields.test.d.ts +0 -1
  147. package/dist/core/ScheduleFields.test.js +0 -193
  148. package/dist/core/SetupState.test.d.ts +0 -1
  149. package/dist/core/SetupState.test.js +0 -113
  150. package/dist/core/SignupMonetizationSettings.test.d.ts +0 -1
  151. package/dist/core/SignupMonetizationSettings.test.js +0 -515
  152. package/dist/core/Stats.test.d.ts +0 -1
  153. package/dist/core/Stats.test.js +0 -36
  154. package/dist/core/SystemAdmin.test.d.ts +0 -1
  155. package/dist/core/SystemAdmin.test.js +0 -1106
  156. package/dist/core/TaskActivitySync.test.d.ts +0 -1
  157. package/dist/core/TaskActivitySync.test.js +0 -90
  158. package/dist/core/TaskAttachmentsCanonical.test.d.ts +0 -1
  159. package/dist/core/TaskAttachmentsCanonical.test.js +0 -356
  160. package/dist/core/TaskAuditTimeline.test.d.ts +0 -1
  161. package/dist/core/TaskAuditTimeline.test.js +0 -319
  162. package/dist/core/TaskTaxonomyValidation.test.d.ts +0 -1
  163. package/dist/core/TaskTaxonomyValidation.test.js +0 -440
  164. package/dist/core/Taskforce.ids.test.d.ts +0 -1
  165. package/dist/core/Taskforce.ids.test.js +0 -611
  166. package/dist/core/Taskforce.listTasksSlim.test.d.ts +0 -1
  167. package/dist/core/Taskforce.listTasksSlim.test.js +0 -91
  168. package/dist/core/Taskforce.mcpAuth.test.d.ts +0 -1
  169. package/dist/core/Taskforce.mcpAuth.test.js +0 -450
  170. package/dist/core/Taskforce.realtimeMutation.test.d.ts +0 -1
  171. package/dist/core/Taskforce.realtimeMutation.test.js +0 -131
  172. package/dist/core/UserProfileAvatarDrafts.test.d.ts +0 -1
  173. package/dist/core/UserProfileAvatarDrafts.test.js +0 -138
  174. package/dist/core/WorkspaceDelete.test.d.ts +0 -1
  175. package/dist/core/WorkspaceDelete.test.js +0 -159
  176. package/dist/core/WorkspacePermissions.test.d.ts +0 -1
  177. package/dist/core/WorkspacePermissions.test.js +0 -270
  178. package/dist/core/WorkspacePlanMode.test.d.ts +0 -1
  179. package/dist/core/WorkspacePlanMode.test.js +0 -383
  180. package/dist/core/WorkspaceSyncSettings.test.d.ts +0 -1
  181. package/dist/core/WorkspaceSyncSettings.test.js +0 -65
  182. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +0 -1
  183. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +0 -212
  184. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +0 -1
  185. package/dist/documentReviews/DocumentReviewSessionStore.test.js +0 -127
  186. package/dist/hooks/authGuardPolicy.test.d.ts +0 -1
  187. package/dist/hooks/authGuardPolicy.test.js +0 -76
  188. package/dist/hooks/bootstrapTimeouts.test.d.ts +0 -1
  189. package/dist/hooks/bootstrapTimeouts.test.js +0 -12
  190. package/dist/hooks/sync/controlPlane.test.d.ts +0 -1
  191. package/dist/hooks/sync/controlPlane.test.js +0 -121
  192. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +0 -1
  193. package/dist/hooks/sync/lifecyclePolicy.test.js +0 -124
  194. package/dist/hooks/sync/orchestratorShared.test.d.ts +0 -1
  195. package/dist/hooks/sync/orchestratorShared.test.js +0 -98
  196. package/dist/hooks/sync/pullLifecycle.test.d.ts +0 -1
  197. package/dist/hooks/sync/pullLifecycle.test.js +0 -80
  198. package/dist/hooks/sync/recovery.test.d.ts +0 -1
  199. package/dist/hooks/sync/recovery.test.js +0 -292
  200. package/dist/hooks/sync/transfers.test.d.ts +0 -1
  201. package/dist/hooks/sync/transfers.test.js +0 -928
  202. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +0 -1
  203. package/dist/hooks/sync/useRecentSyncEvents.test.js +0 -124
  204. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +0 -1
  205. package/dist/hooks/sync/useSyncStatusActions.test.js +0 -133
  206. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +0 -1
  207. package/dist/hooks/sync/useSyncStatusControls.test.js +0 -88
  208. package/dist/hooks/useFilterSort.test.d.ts +0 -1
  209. package/dist/hooks/useFilterSort.test.js +0 -419
  210. package/dist/hooks/useRealtimeSync.test.d.ts +0 -1
  211. package/dist/hooks/useRealtimeSync.test.js +0 -311
  212. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +0 -1
  213. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +0 -54
  214. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +0 -1
  215. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +0 -228
  216. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +0 -1
  217. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +0 -2027
  218. package/dist/hooks/useTaskData.test.d.ts +0 -1
  219. package/dist/hooks/useTaskData.test.js +0 -291
  220. package/dist/hooks/useTaskMutations.test.d.ts +0 -1
  221. package/dist/hooks/useTaskMutations.test.js +0 -256
  222. package/dist/hooks/useTaskforce.contract.test.d.ts +0 -1
  223. package/dist/hooks/useTaskforce.contract.test.js +0 -7
  224. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +0 -1
  225. package/dist/hooks/useTaskforce.priority-filters.test.js +0 -28
  226. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +0 -1
  227. package/dist/hooks/useTaskforce.runtime-routing.test.js +0 -246
  228. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +0 -1
  229. package/dist/hooks/useTaskforce.sync-behavior.test.js +0 -3644
  230. package/dist/hooks/useWorkspaceSyncController.test.d.ts +0 -1
  231. package/dist/hooks/useWorkspaceSyncController.test.js +0 -192
  232. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +0 -1
  233. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +0 -77
  234. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +0 -1
  235. package/dist/hooks/workspace/workspaceLocalState.test.js +0 -59
  236. package/dist/localization/localizationCatalogs.test.d.ts +0 -1
  237. package/dist/localization/localizationCatalogs.test.js +0 -37
  238. package/dist/mcp/aiProfileBootstrap.test.d.ts +0 -1
  239. package/dist/mcp/aiProfileBootstrap.test.js +0 -68
  240. package/dist/mcp/canonicalAssetHelpers.test.d.ts +0 -1
  241. package/dist/mcp/canonicalAssetHelpers.test.js +0 -67
  242. package/dist/mcp/clientIntegrations.test.d.ts +0 -1
  243. package/dist/mcp/clientIntegrations.test.js +0 -219
  244. package/dist/mcp/optionResolution.test.d.ts +0 -1
  245. package/dist/mcp/optionResolution.test.js +0 -43
  246. package/dist/mcp/runtime.test.d.ts +0 -1
  247. package/dist/mcp/runtime.test.js +0 -5211
  248. package/dist/mcp/structuredCommentValidation.test.d.ts +0 -1
  249. package/dist/mcp/structuredCommentValidation.test.js +0 -31
  250. package/dist/mcp/structuredDocValidation.test.d.ts +0 -1
  251. package/dist/mcp/structuredDocValidation.test.js +0 -35
  252. package/dist/mcp/toolRegistry.test.d.ts +0 -1
  253. package/dist/mcp/toolRegistry.test.js +0 -34
  254. package/dist/migrations/billingSchemaParity.test.d.ts +0 -1
  255. package/dist/migrations/billingSchemaParity.test.js +0 -160
  256. package/dist/migrations/taskSchemaMigrations.test.d.ts +0 -1
  257. package/dist/migrations/taskSchemaMigrations.test.js +0 -225
  258. package/dist/qaOpen.test.d.ts +0 -1
  259. package/dist/qaOpen.test.js +0 -56
  260. package/dist/releaseVersion.test.d.ts +0 -1
  261. package/dist/releaseVersion.test.js +0 -32
  262. package/dist/runtime/appGates.test.d.ts +0 -1
  263. package/dist/runtime/appGates.test.js +0 -42
  264. package/dist/server/aiProfileAvatarAssets.test.d.ts +0 -1
  265. package/dist/server/aiProfileAvatarAssets.test.js +0 -213
  266. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +0 -1
  267. package/dist/server/annotatedAttachmentsRoutes.test.js +0 -532
  268. package/dist/server/auth.test.d.ts +0 -1
  269. package/dist/server/auth.test.js +0 -131
  270. package/dist/server/buildInfo.test.d.ts +0 -1
  271. package/dist/server/buildInfo.test.js +0 -29
  272. package/dist/server/cors.test.d.ts +0 -1
  273. package/dist/server/cors.test.js +0 -77
  274. package/dist/server/documentReviewRoutes.test.d.ts +0 -1
  275. package/dist/server/documentReviewRoutes.test.js +0 -190
  276. package/dist/server/email.test.d.ts +0 -1
  277. package/dist/server/email.test.js +0 -113
  278. package/dist/server/index.cookieProxy.test.d.ts +0 -1
  279. package/dist/server/index.cookieProxy.test.js +0 -67
  280. package/dist/server/index.rateLimit.test.d.ts +0 -1
  281. package/dist/server/index.rateLimit.test.js +0 -22
  282. package/dist/server/index.test.d.ts +0 -1
  283. package/dist/server/index.test.js +0 -283
  284. package/dist/server/rateLimit.test.d.ts +0 -1
  285. package/dist/server/rateLimit.test.js +0 -107
  286. package/dist/server/realtimeSync.auth.test.d.ts +0 -1
  287. package/dist/server/realtimeSync.auth.test.js +0 -62
  288. package/dist/server/realtimeSync.replay.test.d.ts +0 -1
  289. package/dist/server/realtimeSync.replay.test.js +0 -192
  290. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +0 -1
  291. package/dist/server/realtimeSync.workspaceIsolation.test.js +0 -101
  292. package/dist/server/routes/billing.test.d.ts +0 -1
  293. package/dist/server/routes/billing.test.js +0 -3515
  294. package/dist/server/routes/shared.test.d.ts +0 -1
  295. package/dist/server/routes/shared.test.js +0 -32
  296. package/dist/server/routes/sync.integration.test.d.ts +0 -1
  297. package/dist/server/routes/sync.integration.test.js +0 -2932
  298. package/dist/server/routes/syncAuxRoutes.test.d.ts +0 -1
  299. package/dist/server/routes/syncAuxRoutes.test.js +0 -158
  300. package/dist/server/routes/syncRouteGuards.test.d.ts +0 -1
  301. package/dist/server/routes/syncRouteGuards.test.js +0 -94
  302. package/dist/server/routes.test.d.ts +0 -1
  303. package/dist/server/routes.test.js +0 -15612
  304. package/dist/server/runtimeMode.test.d.ts +0 -1
  305. package/dist/server/runtimeMode.test.js +0 -26
  306. package/dist/server/securityHeaders.test.d.ts +0 -1
  307. package/dist/server/securityHeaders.test.js +0 -70
  308. package/dist/services/templateEngine.test.d.ts +0 -1
  309. package/dist/services/templateEngine.test.js +0 -150
  310. package/dist/services/templates.test.d.ts +0 -1
  311. package/dist/services/templates.test.js +0 -45
  312. package/dist/services/workflowDocumentFixtures.test.d.ts +0 -1
  313. package/dist/services/workflowDocumentFixtures.test.js +0 -78
  314. package/dist/services/workflowExportMatrix.test.d.ts +0 -1
  315. package/dist/services/workflowExportMatrix.test.js +0 -113
  316. package/dist/services/workflowExportSnapshots.test.d.ts +0 -1
  317. package/dist/services/workflowExportSnapshots.test.js +0 -30
  318. package/dist/services/workflowGeneration.test.d.ts +0 -1
  319. package/dist/services/workflowGeneration.test.js +0 -12
  320. package/dist/services/workflowSimulation.test.d.ts +0 -1
  321. package/dist/services/workflowSimulation.test.js +0 -113
  322. package/dist/services/workflowV2Policy.test.d.ts +0 -1
  323. package/dist/services/workflowV2Policy.test.js +0 -80
  324. package/dist/shared/customTaxonomyLibrary.test.d.ts +0 -1
  325. package/dist/shared/customTaxonomyLibrary.test.js +0 -20
  326. package/dist/shared/passwordPolicy.test.d.ts +0 -1
  327. package/dist/shared/passwordPolicy.test.js +0 -34
  328. package/dist/shared/runtimeContract.test.d.ts +0 -1
  329. package/dist/shared/runtimeContract.test.js +0 -23
  330. package/dist/shared/taxonomyLibrary.test.d.ts +0 -1
  331. package/dist/shared/taxonomyLibrary.test.js +0 -26
  332. package/dist/shared/taxonomyState.test.d.ts +0 -1
  333. package/dist/shared/taxonomyState.test.js +0 -71
  334. package/dist/soak/preflight.cli.d.ts +0 -1
  335. package/dist/soak/preflight.cli.js +0 -28
  336. package/dist/soak/preflight.d.ts +0 -26
  337. package/dist/soak/preflight.js +0 -113
  338. package/dist/soak/preflight.test.d.ts +0 -1
  339. package/dist/soak/preflight.test.js +0 -39
  340. package/dist/storage/objectStorageClient.test.d.ts +0 -1
  341. package/dist/storage/objectStorageClient.test.js +0 -82
  342. package/dist/storage/objectStorageConfig.test.d.ts +0 -1
  343. package/dist/storage/objectStorageConfig.test.js +0 -50
  344. package/dist/storage/postgresAdapter.test.d.ts +0 -1
  345. package/dist/storage/postgresAdapter.test.js +0 -29
  346. package/dist/storage/workspaceAssetStore.test.d.ts +0 -1
  347. package/dist/storage/workspaceAssetStore.test.js +0 -588
  348. package/dist/sync/cloudSyncApi.test.d.ts +0 -1
  349. package/dist/sync/cloudSyncApi.test.js +0 -111
  350. package/dist/sync/collaborationSyncPayload.test.d.ts +0 -1
  351. package/dist/sync/collaborationSyncPayload.test.js +0 -125
  352. package/dist/sync/contentSyncPayload.test.d.ts +0 -1
  353. package/dist/sync/contentSyncPayload.test.js +0 -143
  354. package/dist/sync/encryptionService.test.d.ts +0 -1
  355. package/dist/sync/encryptionService.test.js +0 -52
  356. package/dist/sync/planningSyncPayload.test.d.ts +0 -1
  357. package/dist/sync/planningSyncPayload.test.js +0 -92
  358. package/dist/sync/realtimeFeature.test.d.ts +0 -1
  359. package/dist/sync/realtimeFeature.test.js +0 -30
  360. package/dist/sync/realtimeReconcile.test.d.ts +0 -1
  361. package/dist/sync/realtimeReconcile.test.js +0 -43
  362. package/dist/sync/syncApplyHandlers.test.d.ts +0 -1
  363. package/dist/sync/syncApplyHandlers.test.js +0 -626
  364. package/dist/sync/syncService.test.d.ts +0 -1
  365. package/dist/sync/syncService.test.js +0 -903
  366. package/dist/sync/taskSyncPayload.test.d.ts +0 -1
  367. package/dist/sync/taskSyncPayload.test.js +0 -54
  368. package/dist/sync/workspacePullFeed.test.d.ts +0 -1
  369. package/dist/sync/workspacePullFeed.test.js +0 -438
  370. package/dist/sync/workspaceSyncModel.test.d.ts +0 -1
  371. package/dist/sync/workspaceSyncModel.test.js +0 -1442
  372. package/dist/test/setup.d.ts +0 -1
  373. package/dist/test/setup.js +0 -15
  374. package/dist/ui/assets/documentReferences-BPWp6ce0.js +0 -1
  375. package/dist/ui/assets/index-zvbo2Jse.js +0 -5
  376. package/dist/utils/accountProfileSummaryCache.test.d.ts +0 -1
  377. package/dist/utils/accountProfileSummaryCache.test.js +0 -63
  378. package/dist/utils/annotatedAttachments.test.d.ts +0 -1
  379. package/dist/utils/annotatedAttachments.test.js +0 -108
  380. package/dist/utils/assignees.test.d.ts +0 -1
  381. package/dist/utils/assignees.test.js +0 -25
  382. package/dist/utils/avatarUpload.test.d.ts +0 -1
  383. package/dist/utils/avatarUpload.test.js +0 -40
  384. package/dist/utils/canonicalAssetKind.test.d.ts +0 -1
  385. package/dist/utils/canonicalAssetKind.test.js +0 -24
  386. package/dist/utils/coalescedRefresh.test.d.ts +0 -1
  387. package/dist/utils/coalescedRefresh.test.js +0 -30
  388. package/dist/utils/constants.icons.test.d.ts +0 -1
  389. package/dist/utils/constants.icons.test.js +0 -41
  390. package/dist/utils/planParser.test.d.ts +0 -1
  391. package/dist/utils/planParser.test.js +0 -183
  392. package/dist/utils/planningReferences.test.d.ts +0 -1
  393. package/dist/utils/planningReferences.test.js +0 -17
  394. package/dist/utils/supportContact.test.d.ts +0 -1
  395. package/dist/utils/supportContact.test.js +0 -19
  396. package/dist/utils/syncEventDetails.test.d.ts +0 -1
  397. package/dist/utils/syncEventDetails.test.js +0 -23
  398. package/dist/utils/syncEventPresentation.test.d.ts +0 -1
  399. package/dist/utils/syncEventPresentation.test.js +0 -64
  400. package/dist/utils/syncStatusPresentation.test.d.ts +0 -1
  401. package/dist/utils/syncStatusPresentation.test.js +0 -164
  402. package/dist/utils/taskActivity.test.d.ts +0 -1
  403. package/dist/utils/taskActivity.test.js +0 -218
  404. package/dist/utils/taskDraftDefaults.test.d.ts +0 -1
  405. package/dist/utils/taskDraftDefaults.test.js +0 -34
  406. package/dist/utils/taskDragZones.test.d.ts +0 -1
  407. package/dist/utils/taskDragZones.test.js +0 -14
  408. package/dist/utils/taskNormalization.test.d.ts +0 -1
  409. package/dist/utils/taskNormalization.test.js +0 -158
  410. package/dist/utils/taskSearch.test.d.ts +0 -1
  411. package/dist/utils/taskSearch.test.js +0 -69
  412. package/dist/utils/taskUtils.test.d.ts +0 -1
  413. package/dist/utils/taskUtils.test.js +0 -19
  414. package/dist/utils/taskforceApiClient.test.d.ts +0 -1
  415. package/dist/utils/taskforceApiClient.test.js +0 -44
  416. package/dist/utils/taxonomyFormDefaults.test.d.ts +0 -1
  417. package/dist/utils/taxonomyFormDefaults.test.js +0 -63
  418. package/dist/utils/uiNotice.test.d.ts +0 -1
  419. package/dist/utils/uiNotice.test.js +0 -18
  420. package/dist/utils/workspaceIdentity.test.d.ts +0 -1
  421. package/dist/utils/workspaceIdentity.test.js +0 -61
  422. package/dist/utils/workspaceLifecycle.test.d.ts +0 -1
  423. package/dist/utils/workspaceLifecycle.test.js +0 -64
  424. package/dist/utils/workspaceSyncPresentation.test.d.ts +0 -1
  425. package/dist/utils/workspaceSyncPresentation.test.js +0 -71
  426. package/scripts/bootstrap-playwright-auth.mjs +0 -155
  427. package/scripts/check-cloud-mcp.mjs +0 -83
  428. package/scripts/check-compat-boundaries.mjs +0 -41
  429. package/scripts/check-env.ts +0 -31
  430. package/scripts/check-localization-strings.mjs +0 -49
  431. package/scripts/check-runtime-boundaries.mjs +0 -44
  432. package/scripts/debug-global.mjs +0 -34
  433. package/scripts/debug-login.mjs +0 -32
  434. package/scripts/debug-server.mjs +0 -36
  435. package/scripts/deploy-prod.sh +0 -41
  436. package/scripts/deploy-staging.sh +0 -23
  437. package/scripts/export-sqlite-to-postgres.mjs +0 -123
  438. package/scripts/generate-env-example.ts +0 -8
  439. package/scripts/mcp-stdio-adapter.mjs +0 -160
  440. package/scripts/migrate-task-data-assets.mjs +0 -408
  441. package/scripts/playwright-auth.sh +0 -19
  442. package/scripts/qa-open-lib.mjs +0 -175
  443. package/scripts/qa-open-lib.mjs.d.ts +0 -49
  444. package/scripts/qa-open.mjs +0 -352
  445. package/scripts/release-version-lib.mjs +0 -88
  446. package/scripts/release-version.mjs +0 -72
  447. package/scripts/run-billing-performance-smoke.sh +0 -24
  448. package/scripts/run-dev-proxy.sh +0 -51
  449. package/scripts/run-e2e-realtime.sh +0 -30
  450. package/scripts/run-smoke.sh +0 -35
  451. package/scripts/run-soak.sh +0 -41
  452. package/scripts/test-global.js +0 -143
  453. package/scripts/update-homebrew-formula.sh +0 -108
  454. package/scripts/with-esbuild.sh +0 -11
@@ -1,2883 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4
- import userEvent from '@testing-library/user-event';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { AnnotatedAttachmentWorkspaceShell } from './AnnotatedAttachmentWorkspace';
7
- const { fetchTaskforceApi } = vi.hoisted(() => ({
8
- fetchTaskforceApi: vi.fn(),
9
- }));
10
- vi.mock('../../utils/taskforceApiClient', () => ({
11
- fetchTaskforceApi,
12
- resolveTaskforceApiUrl: (path) => path,
13
- }));
14
- function buildSessionResponse(overrides) {
15
- return {
16
- id: 'session-1',
17
- tenantId: 'tenant-1',
18
- workspaceId: 'workspace-1',
19
- taskId: 'task-1',
20
- baseImageAssetId: 'asset-image-1',
21
- title: 'Homepage shot review',
22
- globalInstruction: '',
23
- annotations: [],
24
- createdByActorId: 'user-1',
25
- updatedByActorId: 'user-1',
26
- createdAt: '2026-03-25T12:00:00.000Z',
27
- updatedAt: '2026-03-25T12:00:00.000Z',
28
- deletedAt: null,
29
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
30
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
31
- ...overrides,
32
- };
33
- }
34
- function mockAutoCreateFirstSession(sessionOverrides) {
35
- const session = buildSessionResponse(sessionOverrides);
36
- fetchTaskforceApi
37
- .mockResolvedValueOnce({
38
- ok: true,
39
- json: async () => ({
40
- sessions: [],
41
- }),
42
- })
43
- .mockResolvedValueOnce({
44
- ok: true,
45
- json: async () => ({
46
- session,
47
- }),
48
- });
49
- return session;
50
- }
51
- function escapeForRegex(value) {
52
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
- }
54
- async function findSessionCard(title) {
55
- const pattern = new RegExp(escapeForRegex(title), 'i');
56
- const matchingInput = screen.queryByDisplayValue(pattern);
57
- if (matchingInput)
58
- return matchingInput;
59
- const matchingText = await screen.findByText(pattern);
60
- return matchingText.closest('button') ?? matchingText;
61
- }
62
- describe('AnnotatedAttachmentWorkspaceShell', () => {
63
- beforeEach(() => {
64
- fetchTaskforceApi.mockReset();
65
- if (typeof window.ResizeObserver === 'undefined') {
66
- class ResizeObserverMock {
67
- observe() { }
68
- disconnect() { }
69
- unobserve() { }
70
- }
71
- Object.defineProperty(window, 'ResizeObserver', {
72
- configurable: true,
73
- writable: true,
74
- value: ResizeObserverMock,
75
- });
76
- }
77
- });
78
- afterEach(() => {
79
- vi.useRealTimers();
80
- vi.restoreAllMocks();
81
- });
82
- it('shows an image loading indicator until the canvas image finishes loading', async () => {
83
- fetchTaskforceApi.mockResolvedValueOnce({
84
- ok: true,
85
- json: async () => ({
86
- sessions: [
87
- {
88
- id: 'session-1',
89
- tenantId: 'tenant-1',
90
- workspaceId: 'workspace-1',
91
- taskId: 'task-1',
92
- baseImageAssetId: 'asset-image-1',
93
- title: 'Homepage review',
94
- globalInstruction: 'Review this screenshot',
95
- annotations: [
96
- {
97
- id: 'annotation-1',
98
- order: 0,
99
- kind: 'pin',
100
- x: 0.2,
101
- y: 0.3,
102
- instruction: 'Inspect this button',
103
- markerType: 'question',
104
- color: '#0f766e',
105
- },
106
- ],
107
- createdByActorId: 'user-1',
108
- updatedByActorId: 'user-1',
109
- createdAt: '2026-03-25T12:00:00.000Z',
110
- updatedAt: '2026-03-25T12:00:00.000Z',
111
- deletedAt: null,
112
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
113
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
114
- },
115
- ],
116
- }),
117
- });
118
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
119
- taskId: 'task-1',
120
- taskReferenceLabel: 'T-101',
121
- imageReferenceLabel: 'I-22',
122
- assetId: 'asset-image-1',
123
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
124
- displayName: 'Homepage shot',
125
- } }));
126
- expect(await screen.findByText('Loading image…')).toBeInTheDocument();
127
- expect(screen.queryByTestId('annotated-attachment-overlay')).not.toBeInTheDocument();
128
- fireEvent.load(screen.getByAltText('Homepage shot'));
129
- await waitFor(() => {
130
- expect(screen.queryByText('Loading image…')).not.toBeInTheDocument();
131
- });
132
- expect(screen.getByTestId('annotated-attachment-overlay')).toBeInTheDocument();
133
- });
134
- it('auto-creates the first session and still renders the image while it loads', async () => {
135
- mockAutoCreateFirstSession();
136
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
137
- taskId: 'task-1',
138
- taskReferenceLabel: 'T-101',
139
- assetId: 'asset-image-1',
140
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
141
- displayName: 'Homepage shot',
142
- } }));
143
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
144
- expect(screen.getByText('Loading image…')).toBeInTheDocument();
145
- fireEvent.load(screen.getByAltText('Homepage shot'));
146
- await waitFor(() => {
147
- expect(screen.queryByText('Loading image…')).not.toBeInTheDocument();
148
- });
149
- expect(screen.getByAltText('Homepage shot')).toBeInTheDocument();
150
- expect(screen.getByTestId('annotated-attachment-overlay')).toBeInTheDocument();
151
- });
152
- it('warns and offers to create a session when a marker tool is clicked without an active session', async () => {
153
- const user = userEvent.setup();
154
- const session = buildSessionResponse({
155
- id: 'session-recovered-1',
156
- title: 'Recovered review',
157
- taskId: 'task-1',
158
- });
159
- fetchTaskforceApi
160
- .mockResolvedValueOnce({
161
- ok: true,
162
- json: async () => ({ sessions: [] }),
163
- })
164
- .mockResolvedValueOnce({
165
- ok: false,
166
- json: async () => ({ error: 'Failed to create session.' }),
167
- })
168
- .mockResolvedValueOnce({
169
- ok: true,
170
- json: async () => ({ session }),
171
- })
172
- .mockResolvedValueOnce({
173
- ok: true,
174
- json: async () => ({ sessions: [session] }),
175
- });
176
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
177
- taskId: 'task-1',
178
- taskReferenceLabel: 'T-101',
179
- assetId: 'asset-image-1',
180
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
181
- displayName: 'Homepage shot',
182
- } }));
183
- expect(await screen.findByText('Failed to create session.')).toBeInTheDocument();
184
- await user.click(screen.getByRole('button', { name: /pin tool/i }));
185
- expect(screen.getByText('No session exists for this image yet.')).toBeInTheDocument();
186
- const createNow = screen.getByRole('button', { name: /create one now/i });
187
- expect(createNow).toBeInTheDocument();
188
- await user.click(createNow);
189
- expect(await findSessionCard('Recovered review')).toBeInTheDocument();
190
- });
191
- it('pastes a clipboard image into a new unattached image-notes session', async () => {
192
- const user = userEvent.setup();
193
- const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
194
- ok: true,
195
- json: async () => ({
196
- success: true,
197
- assetId: 'asset-pasted-1',
198
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-pasted-1.png',
199
- displayName: 'Pasted image',
200
- referenceLabel: 'I-24',
201
- }),
202
- });
203
- Object.defineProperty(globalThis.navigator, 'clipboard', {
204
- configurable: true,
205
- value: {
206
- read: vi.fn().mockResolvedValue([
207
- {
208
- types: ['image/png'],
209
- getType: vi.fn().mockResolvedValue(new Blob(['png'], { type: 'image/png' })),
210
- },
211
- ]),
212
- },
213
- });
214
- const fileReaderReadAsDataUrl = vi
215
- .spyOn(FileReader.prototype, 'readAsDataURL')
216
- .mockImplementation(function mockReadAsDataUrl() {
217
- Object.defineProperty(this, 'result', {
218
- configurable: true,
219
- value: 'data:image/png;base64,cG5n',
220
- });
221
- this.onload?.call(this, new ProgressEvent('load'));
222
- });
223
- fetchTaskforceApi
224
- .mockResolvedValueOnce({
225
- ok: true,
226
- json: async () => ({ sessions: [] }),
227
- })
228
- .mockResolvedValueOnce({
229
- ok: true,
230
- json: async () => ({
231
- session: buildSessionResponse({
232
- id: 'session-pasted-1',
233
- taskId: '',
234
- baseImageAssetId: 'asset-pasted-1',
235
- title: 'Pasted image review',
236
- }),
237
- }),
238
- });
239
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1" }));
240
- await user.click(screen.getByRole('button', { name: /paste image/i }));
241
- await waitFor(() => expect(fetchSpy).toHaveBeenCalledWith('/api/taskforce/context-upload', expect.objectContaining({
242
- method: 'POST',
243
- credentials: 'include',
244
- })));
245
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&imageAssetId=asset-pasted-1', expect.any(Object), ''));
246
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({
247
- method: 'POST',
248
- body: expect.stringContaining('"baseImageAssetId":"asset-pasted-1"'),
249
- }), ''));
250
- expect(await screen.findByText('Unattached Image')).toBeInTheDocument();
251
- expect(await findSessionCard('Pasted image review')).toBeInTheDocument();
252
- fetchSpy.mockRestore();
253
- fileReaderReadAsDataUrl.mockRestore();
254
- });
255
- it('opens an image by reference from the toolbar modal', async () => {
256
- const user = userEvent.setup();
257
- fetchTaskforceApi
258
- .mockResolvedValueOnce({
259
- ok: true,
260
- json: async () => ({
261
- sessions: [
262
- buildSessionResponse({
263
- id: 'session-old-1',
264
- title: 'Homepage shot review',
265
- taskId: 'task-1',
266
- baseImageAssetId: 'asset-image-1',
267
- }),
268
- ],
269
- }),
270
- })
271
- .mockResolvedValueOnce({
272
- ok: true,
273
- json: async () => ({
274
- target: {
275
- assetId: 'asset-image-24',
276
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-24.png',
277
- displayName: 'Reference image',
278
- imageReferenceLabel: 'I-24',
279
- },
280
- }),
281
- })
282
- .mockResolvedValueOnce({
283
- ok: true,
284
- json: async () => ({ sessions: [] }),
285
- })
286
- .mockResolvedValueOnce({
287
- ok: true,
288
- json: async () => ({
289
- session: buildSessionResponse({
290
- id: 'session-ref-1',
291
- taskId: '',
292
- baseImageAssetId: 'asset-image-24',
293
- title: 'Reference image review',
294
- }),
295
- }),
296
- });
297
- function ControlledOpenHarness() {
298
- const [target, setTarget] = React.useState({
299
- taskId: 'task-1',
300
- taskReferenceLabel: 'T-101',
301
- imageReferenceLabel: 'I-22',
302
- assetId: 'asset-image-1',
303
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
304
- displayName: 'Homepage shot',
305
- });
306
- const [sessionId, setSessionId] = React.useState(null);
307
- return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
308
- setTarget(nextTarget);
309
- setSessionId(options?.sessionId ?? null);
310
- } }));
311
- }
312
- render(_jsx(ControlledOpenHarness, {}));
313
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
314
- fireEvent.load(screen.getByAltText('Homepage shot'));
315
- await user.click(screen.getByRole('button', { name: /open image/i }));
316
- await user.type(screen.getByLabelText(/image reference number/i), 'I-24');
317
- await user.click(screen.getByRole('button', { name: /^open$/i }));
318
- await waitFor(() => {
319
- expect(screen.getByAltText('Reference image')).toBeInTheDocument();
320
- expect(screen.queryByAltText('Homepage shot')).not.toBeInTheDocument();
321
- });
322
- });
323
- it('loads images from the image tray and opens the selected image', async () => {
324
- const user = userEvent.setup();
325
- fetchTaskforceApi
326
- .mockResolvedValueOnce({
327
- ok: true,
328
- json: async () => ({
329
- images: [
330
- {
331
- assetId: 'asset-image-24',
332
- imageReferenceLabel: 'I-24',
333
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-24.png',
334
- displayName: 'Reference image',
335
- originalFilename: 'reference.png',
336
- sizeBytes: 2048,
337
- updatedAt: '2026-03-25T12:00:00.000Z',
338
- attachmentCount: 0,
339
- sessionCount: 0,
340
- },
341
- ],
342
- }),
343
- })
344
- .mockResolvedValueOnce({
345
- ok: true,
346
- json: async () => ({ sessions: [] }),
347
- })
348
- .mockResolvedValueOnce({
349
- ok: true,
350
- json: async () => ({
351
- session: buildSessionResponse({
352
- id: 'session-ref-1',
353
- taskId: '',
354
- baseImageAssetId: 'asset-image-24',
355
- title: 'Reference image review',
356
- }),
357
- }),
358
- });
359
- function ControlledImageTrayHarness() {
360
- const [target, setTarget] = React.useState(null);
361
- const [sessionId, setSessionId] = React.useState(null);
362
- return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", imageTrayOpen: true, requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
363
- setTarget(nextTarget);
364
- setSessionId(options?.sessionId ?? null);
365
- } }));
366
- }
367
- render(_jsx(ControlledImageTrayHarness, {}));
368
- expect(await screen.findByRole('button', { name: /reference image/i })).toBeInTheDocument();
369
- await user.click(screen.getByRole('button', { name: /reference image/i }));
370
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/images?workspaceId=workspace-1', expect.objectContaining({ cache: 'no-store' }), ''));
371
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&imageAssetId=asset-image-24', expect.any(Object), ''));
372
- expect(await screen.findByAltText('Reference image')).toBeInTheDocument();
373
- expect(await findSessionCard('Reference image review')).toBeInTheDocument();
374
- });
375
- it('does not revert to the previous image while opening a pasted image in controlled mode', async () => {
376
- const user = userEvent.setup();
377
- const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
378
- ok: true,
379
- json: async () => ({
380
- success: true,
381
- assetId: 'asset-pasted-1',
382
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-pasted-1.png',
383
- displayName: 'Pasted image',
384
- referenceLabel: 'I-24',
385
- }),
386
- });
387
- Object.defineProperty(globalThis.navigator, 'clipboard', {
388
- configurable: true,
389
- value: {
390
- read: vi.fn().mockResolvedValue([
391
- {
392
- types: ['image/png'],
393
- getType: vi.fn().mockResolvedValue(new Blob(['png'], { type: 'image/png' })),
394
- },
395
- ]),
396
- },
397
- });
398
- const fileReaderReadAsDataUrl = vi
399
- .spyOn(FileReader.prototype, 'readAsDataURL')
400
- .mockImplementation(function mockReadAsDataUrl() {
401
- Object.defineProperty(this, 'result', {
402
- configurable: true,
403
- value: 'data:image/png;base64,cG5n',
404
- });
405
- this.onload?.call(this, new ProgressEvent('load'));
406
- });
407
- fetchTaskforceApi
408
- .mockResolvedValueOnce({
409
- ok: true,
410
- json: async () => ({
411
- sessions: [
412
- buildSessionResponse({
413
- id: 'session-old-1',
414
- title: 'Homepage shot review',
415
- taskId: 'task-1',
416
- baseImageAssetId: 'asset-image-1',
417
- }),
418
- ],
419
- }),
420
- })
421
- .mockResolvedValueOnce({
422
- ok: true,
423
- json: async () => ({ sessions: [] }),
424
- })
425
- .mockResolvedValueOnce({
426
- ok: true,
427
- json: async () => ({
428
- session: buildSessionResponse({
429
- id: 'session-pasted-1',
430
- taskId: '',
431
- baseImageAssetId: 'asset-pasted-1',
432
- title: 'Pasted image review',
433
- }),
434
- }),
435
- });
436
- function ControlledPasteHarness() {
437
- const [target, setTarget] = React.useState({
438
- taskId: 'task-1',
439
- taskReferenceLabel: 'T-101',
440
- imageReferenceLabel: 'I-22',
441
- assetId: 'asset-image-1',
442
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
443
- displayName: 'Homepage shot',
444
- });
445
- const [sessionId, setSessionId] = React.useState(null);
446
- return (_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target, requestedSessionId: sessionId, onOpenTarget: (nextTarget, options) => {
447
- setTarget(nextTarget);
448
- setSessionId(options?.sessionId ?? null);
449
- } }));
450
- }
451
- render(_jsx(ControlledPasteHarness, {}));
452
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
453
- fireEvent.load(screen.getByAltText('Homepage shot'));
454
- await user.click(screen.getByRole('button', { name: /paste image/i }));
455
- await waitFor(() => {
456
- expect(screen.getByAltText('Pasted image')).toBeInTheDocument();
457
- expect(screen.queryByAltText('Homepage shot')).not.toBeInTheDocument();
458
- });
459
- fetchSpy.mockRestore();
460
- fileReaderReadAsDataUrl.mockRestore();
461
- });
462
- it('shows who created each session in the sessions list', async () => {
463
- fetchTaskforceApi.mockResolvedValueOnce({
464
- ok: true,
465
- json: async () => ({
466
- sessions: [
467
- {
468
- id: 'session-1',
469
- tenantId: 'tenant-1',
470
- workspaceId: 'workspace-1',
471
- taskId: 'task-1',
472
- baseImageAssetId: 'asset-image-1',
473
- title: 'Homepage review',
474
- globalInstruction: 'Review this screenshot',
475
- annotations: [],
476
- createdByActorId: 'user-1',
477
- updatedByActorId: 'user-1',
478
- createdAt: '2026-03-25T12:00:00.000Z',
479
- updatedAt: '2026-03-25T12:00:00.000Z',
480
- deletedAt: null,
481
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
482
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
483
- },
484
- ],
485
- }),
486
- });
487
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
488
- taskId: 'task-1',
489
- taskReferenceLabel: 'T-101',
490
- assetId: 'asset-image-1',
491
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
492
- displayName: 'Homepage shot',
493
- } }));
494
- expect(await screen.findByText('Created by Alex')).toBeInTheDocument();
495
- });
496
- it('shows new box and arrow markers immediately while dragging them out', async () => {
497
- const user = userEvent.setup();
498
- fetchTaskforceApi.mockResolvedValueOnce({
499
- ok: true,
500
- json: async () => ({
501
- sessions: [
502
- buildSessionResponse({
503
- title: 'Homepage review',
504
- globalInstruction: 'Review this screenshot',
505
- }),
506
- ],
507
- }),
508
- });
509
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
510
- taskId: 'task-1',
511
- assetId: 'asset-image-1',
512
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
513
- displayName: 'Homepage shot',
514
- } }));
515
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
516
- fireEvent.load(screen.getByAltText('Homepage shot'));
517
- const overlay = screen.getByTestId('annotated-attachment-overlay');
518
- Object.defineProperty(overlay, 'getBoundingClientRect', {
519
- configurable: true,
520
- value: () => ({
521
- left: 0,
522
- top: 0,
523
- width: 100,
524
- height: 100,
525
- right: 100,
526
- bottom: 100,
527
- x: 0,
528
- y: 0,
529
- toJSON: () => ({}),
530
- }),
531
- });
532
- await user.click(screen.getByRole('button', { name: /box tool\./i }));
533
- fireEvent.pointerDown(overlay, { clientX: 20, clientY: 25 });
534
- expect(screen.getByRole('button', { name: 'Box' })).toBeInTheDocument();
535
- fireEvent.pointerMove(overlay, { clientX: 45, clientY: 55 });
536
- fireEvent.pointerUp(overlay, { clientX: 45, clientY: 55 });
537
- await user.click(screen.getByRole('button', { name: /arrow tool\./i }));
538
- fireEvent.pointerDown(overlay, { clientX: 60, clientY: 30 });
539
- expect(screen.getByRole('button', { name: 'Arrow' })).toBeInTheDocument();
540
- fireEvent.pointerMove(overlay, { clientX: 80, clientY: 60 });
541
- fireEvent.pointerUp(overlay, { clientX: 80, clientY: 60 });
542
- });
543
- it('updates the selected marker color and reuses it for new markers', async () => {
544
- const user = userEvent.setup();
545
- fetchTaskforceApi
546
- .mockResolvedValueOnce({
547
- ok: true,
548
- json: async () => ({
549
- sessions: [
550
- buildSessionResponse({
551
- title: 'Homepage review',
552
- globalInstruction: 'Review this screenshot',
553
- annotations: [
554
- {
555
- id: 'annotation-1',
556
- order: 0,
557
- kind: 'pin',
558
- x: 0.2,
559
- y: 0.3,
560
- instruction: 'Inspect this button',
561
- markerType: 'review',
562
- color: '#7c3aed',
563
- },
564
- ],
565
- }),
566
- ],
567
- }),
568
- })
569
- .mockResolvedValueOnce({
570
- ok: true,
571
- json: async () => ({
572
- session: buildSessionResponse({
573
- title: 'Homepage review',
574
- globalInstruction: 'Review this screenshot',
575
- updatedAt: '2026-03-25T12:06:00.000Z',
576
- annotations: [
577
- {
578
- id: 'annotation-1',
579
- order: 0,
580
- kind: 'pin',
581
- x: 0.2,
582
- y: 0.3,
583
- instruction: 'Inspect this button',
584
- markerType: 'review',
585
- color: '#dc2626',
586
- },
587
- ],
588
- }),
589
- }),
590
- })
591
- .mockResolvedValueOnce({
592
- ok: true,
593
- json: async () => ({
594
- session: buildSessionResponse({
595
- title: 'Homepage review',
596
- globalInstruction: 'Review this screenshot',
597
- updatedAt: '2026-03-25T12:07:00.000Z',
598
- annotations: [
599
- {
600
- id: 'annotation-1',
601
- order: 0,
602
- kind: 'pin',
603
- x: 0.2,
604
- y: 0.3,
605
- instruction: 'Inspect this button',
606
- markerType: 'review',
607
- color: '#dc2626',
608
- },
609
- {
610
- id: 'annotation-2',
611
- order: 1,
612
- kind: 'pin',
613
- x: 0.45,
614
- y: 0.55,
615
- instruction: '',
616
- markerType: 'review',
617
- color: '#dc2626',
618
- },
619
- ],
620
- }),
621
- }),
622
- });
623
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
624
- taskId: 'task-1',
625
- assetId: 'asset-image-1',
626
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
627
- displayName: 'Homepage shot',
628
- } }));
629
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
630
- fireEvent.load(screen.getByAltText('Homepage shot'));
631
- const overlay = screen.getByTestId('annotated-attachment-overlay');
632
- Object.defineProperty(overlay, 'getBoundingClientRect', {
633
- configurable: true,
634
- value: () => ({
635
- left: 0,
636
- top: 0,
637
- width: 100,
638
- height: 100,
639
- right: 100,
640
- bottom: 100,
641
- x: 0,
642
- y: 0,
643
- toJSON: () => ({}),
644
- }),
645
- });
646
- await user.click(screen.getByRole('button', { name: '1' }));
647
- await user.click(screen.getByRole('button', { name: /marker color/i }));
648
- await user.click(screen.getByRole('button', { name: /use marker color #dc2626/i }));
649
- await waitFor(() => {
650
- expect(fetchTaskforceApi.mock.calls).toEqual(expect.arrayContaining([
651
- [
652
- '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1',
653
- expect.objectContaining({ method: 'PATCH' }),
654
- '',
655
- ],
656
- ]));
657
- });
658
- const recolorBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
659
- expect(recolorBody.annotations[0]?.color).toBe('#dc2626');
660
- await user.click(screen.getByRole('button', { name: /pin tool\./i }));
661
- fireEvent.pointerDown(overlay, { clientX: 45, clientY: 55 });
662
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
663
- const newMarkerBody = JSON.parse(String(fetchTaskforceApi.mock.calls[2]?.[1]?.body || '{}'));
664
- expect(newMarkerBody.annotations).toHaveLength(2);
665
- expect(newMarkerBody.annotations[1]?.color).toBe('#dc2626');
666
- });
667
- it('shows the selected session state and edits session details inline', async () => {
668
- const user = userEvent.setup();
669
- fetchTaskforceApi.mockResolvedValueOnce({
670
- ok: true,
671
- json: async () => ({
672
- sessions: [
673
- {
674
- id: 'session-1',
675
- tenantId: 'tenant-1',
676
- workspaceId: 'workspace-1',
677
- taskId: 'task-1',
678
- baseImageAssetId: 'asset-image-1',
679
- title: 'Homepage review',
680
- globalInstruction: 'Review this screenshot',
681
- annotations: [],
682
- createdByActorId: 'user-1',
683
- updatedByActorId: 'user-1',
684
- createdAt: '2026-03-25T12:00:00.000Z',
685
- updatedAt: '2026-03-25T12:00:00.000Z',
686
- deletedAt: null,
687
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
688
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
689
- },
690
- ],
691
- }),
692
- });
693
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
694
- taskId: 'task-1',
695
- taskReferenceLabel: 'T-101',
696
- assetId: 'asset-image-1',
697
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
698
- displayName: 'Homepage shot',
699
- } }));
700
- const sessionCard = await findSessionCard('Homepage review');
701
- expect(sessionCard).toHaveAttribute('aria-pressed', 'true');
702
- expect(screen.queryByLabelText('Session title')).not.toBeInTheDocument();
703
- await user.click(sessionCard);
704
- expect(screen.getByLabelText('Session title')).toBeInTheDocument();
705
- expect(screen.getByDisplayValue('Homepage review')).toBeInTheDocument();
706
- expect(screen.getByDisplayValue('Review this screenshot')).toBeInTheDocument();
707
- });
708
- it('closes session inline editing when clicking outside the session card', async () => {
709
- const user = userEvent.setup();
710
- fetchTaskforceApi.mockResolvedValueOnce({
711
- ok: true,
712
- json: async () => ({
713
- sessions: [
714
- {
715
- id: 'session-1',
716
- tenantId: 'tenant-1',
717
- workspaceId: 'workspace-1',
718
- taskId: 'task-1',
719
- baseImageAssetId: 'asset-image-1',
720
- title: 'Homepage review',
721
- globalInstruction: 'Review this screenshot',
722
- annotations: [],
723
- createdByActorId: 'user-1',
724
- updatedByActorId: 'user-1',
725
- createdAt: '2026-03-25T12:00:00.000Z',
726
- updatedAt: '2026-03-25T12:00:00.000Z',
727
- deletedAt: null,
728
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
729
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
730
- },
731
- ],
732
- }),
733
- });
734
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
735
- taskId: 'task-1',
736
- taskReferenceLabel: 'T-101',
737
- assetId: 'asset-image-1',
738
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
739
- displayName: 'Homepage shot',
740
- } }));
741
- await user.click(await findSessionCard('Homepage review'));
742
- expect(screen.getByLabelText('Session title')).toBeInTheDocument();
743
- await user.click(screen.getByRole('button', { name: /create session/i }));
744
- expect(screen.queryByLabelText('Session title')).not.toBeInTheDocument();
745
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
746
- });
747
- it('shows concise titles on the marker tool rail', async () => {
748
- fetchTaskforceApi.mockResolvedValueOnce({
749
- ok: true,
750
- json: async () => ({
751
- sessions: [
752
- {
753
- id: 'session-1',
754
- tenantId: 'tenant-1',
755
- workspaceId: 'workspace-1',
756
- taskId: 'task-1',
757
- baseImageAssetId: 'asset-image-1',
758
- title: 'Homepage review',
759
- globalInstruction: 'Review this screenshot',
760
- annotations: [],
761
- createdByActorId: 'user-1',
762
- updatedByActorId: 'user-1',
763
- createdAt: '2026-03-25T12:00:00.000Z',
764
- updatedAt: '2026-03-25T12:00:00.000Z',
765
- deletedAt: null,
766
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
767
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
768
- },
769
- ],
770
- }),
771
- });
772
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
773
- taskId: 'task-1',
774
- taskReferenceLabel: 'T-101',
775
- assetId: 'asset-image-1',
776
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
777
- displayName: 'Homepage shot',
778
- } }));
779
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
780
- expect(screen.getByRole('button', { name: /pin tool\./i })).toHaveAttribute('title', 'Pin');
781
- expect(screen.getByRole('button', { name: /box tool\./i })).toHaveAttribute('title', 'Box');
782
- });
783
- it('does not label the last editor as the creator when creator details are missing', async () => {
784
- fetchTaskforceApi.mockResolvedValueOnce({
785
- ok: true,
786
- json: async () => ({
787
- sessions: [
788
- {
789
- id: 'session-1',
790
- tenantId: 'tenant-1',
791
- workspaceId: 'workspace-1',
792
- taskId: 'task-1',
793
- baseImageAssetId: 'asset-image-1',
794
- title: 'Homepage review',
795
- globalInstruction: 'Review this screenshot',
796
- annotations: [],
797
- createdByActorId: 'user-1',
798
- updatedByActorId: 'user-2',
799
- createdAt: '2026-03-25T12:00:00.000Z',
800
- updatedAt: '2026-03-25T13:00:00.000Z',
801
- deletedAt: null,
802
- createdByActor: null,
803
- updatedByActor: { actorId: 'user-2', label: 'Taylor', type: 'user' },
804
- },
805
- ],
806
- }),
807
- });
808
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
809
- taskId: 'task-1',
810
- taskReferenceLabel: 'T-101',
811
- assetId: 'asset-image-1',
812
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
813
- displayName: 'Homepage shot',
814
- } }));
815
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
816
- expect(screen.queryByText('Created by Taylor')).not.toBeInTheDocument();
817
- });
818
- it('enables pan controls only after zooming in', async () => {
819
- const user = userEvent.setup();
820
- fetchTaskforceApi.mockResolvedValueOnce({
821
- ok: true,
822
- json: async () => ({
823
- sessions: [
824
- {
825
- id: 'session-1',
826
- tenantId: 'tenant-1',
827
- workspaceId: 'workspace-1',
828
- taskId: 'task-1',
829
- baseImageAssetId: 'asset-image-1',
830
- title: 'Homepage review',
831
- globalInstruction: 'Review this screenshot',
832
- annotations: [],
833
- createdByActorId: 'user-1',
834
- updatedByActorId: 'user-1',
835
- createdAt: '2026-03-25T12:00:00.000Z',
836
- updatedAt: '2026-03-25T12:00:00.000Z',
837
- deletedAt: null,
838
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
839
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
840
- },
841
- ],
842
- }),
843
- });
844
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
845
- taskId: 'task-1',
846
- taskReferenceLabel: 'T-101',
847
- assetId: 'asset-image-1',
848
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
849
- displayName: 'Homepage shot',
850
- } }));
851
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
852
- fireEvent.load(screen.getByAltText('Homepage shot'));
853
- const panButton = screen.getByRole('button', { name: /pan canvas/i });
854
- expect(panButton).toBeDisabled();
855
- expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
856
- await user.click(screen.getByRole('button', { name: /zoom in/i }));
857
- await waitFor(() => {
858
- expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('125%'));
859
- });
860
- expect(screen.getByRole('button', { name: /pan canvas/i })).toBeEnabled();
861
- });
862
- it('allows zooming out below 100 percent for large screenshots', async () => {
863
- const user = userEvent.setup();
864
- fetchTaskforceApi.mockResolvedValueOnce({
865
- ok: true,
866
- json: async () => ({
867
- sessions: [
868
- {
869
- id: 'session-1',
870
- tenantId: 'tenant-1',
871
- workspaceId: 'workspace-1',
872
- taskId: 'task-1',
873
- baseImageAssetId: 'asset-image-1',
874
- title: 'Homepage review',
875
- globalInstruction: 'Review this screenshot',
876
- annotations: [],
877
- createdByActorId: 'user-1',
878
- updatedByActorId: 'user-1',
879
- createdAt: '2026-03-25T12:00:00.000Z',
880
- updatedAt: '2026-03-25T12:00:00.000Z',
881
- deletedAt: null,
882
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
883
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
884
- },
885
- ],
886
- }),
887
- });
888
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
889
- taskId: 'task-1',
890
- taskReferenceLabel: 'T-101',
891
- assetId: 'asset-image-1',
892
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
893
- displayName: 'Large screenshot',
894
- } }));
895
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
896
- fireEvent.load(screen.getByAltText('Large screenshot'));
897
- expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
898
- await user.click(screen.getByRole('button', { name: /zoom out/i }));
899
- await waitFor(() => {
900
- expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('75%'));
901
- });
902
- expect(screen.getByRole('button', { name: /pan canvas/i })).toBeDisabled();
903
- });
904
- it('keeps the image at natural size at 100 percent even in a narrow canvas viewport', async () => {
905
- fetchTaskforceApi.mockResolvedValueOnce({
906
- ok: true,
907
- json: async () => ({
908
- sessions: [
909
- {
910
- id: 'session-1',
911
- tenantId: 'tenant-1',
912
- workspaceId: 'workspace-1',
913
- taskId: 'task-1',
914
- baseImageAssetId: 'asset-image-1',
915
- title: 'Homepage review',
916
- globalInstruction: 'Review this screenshot',
917
- annotations: [],
918
- createdByActorId: 'user-1',
919
- updatedByActorId: 'user-1',
920
- createdAt: '2026-03-25T12:00:00.000Z',
921
- updatedAt: '2026-03-25T12:00:00.000Z',
922
- deletedAt: null,
923
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
924
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
925
- },
926
- ],
927
- }),
928
- });
929
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
930
- taskId: 'task-1',
931
- taskReferenceLabel: 'T-101',
932
- assetId: 'asset-image-1',
933
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
934
- displayName: 'Wide screenshot',
935
- } }));
936
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
937
- const image = screen.getByAltText('Wide screenshot');
938
- Object.defineProperty(image, 'naturalWidth', { configurable: true, value: 1200 });
939
- Object.defineProperty(image, 'naturalHeight', { configurable: true, value: 800 });
940
- const canvasScroller = image.parentElement?.parentElement?.parentElement;
941
- expect(canvasScroller).not.toBeNull();
942
- Object.defineProperty(canvasScroller, 'clientWidth', { configurable: true, value: 400 });
943
- fireEvent.load(image);
944
- await waitFor(() => {
945
- expect(image.parentElement).toHaveStyle({
946
- width: '1200px',
947
- height: '800px',
948
- });
949
- });
950
- expect(screen.getByRole('button', { name: /reset zoom/i })).toHaveAttribute('title', expect.stringContaining('100%'));
951
- expect(screen.getByRole('button', { name: /pan canvas/i })).toBeEnabled();
952
- });
953
- it('does not reload sessions when rerendered with the same annotated target', async () => {
954
- fetchTaskforceApi.mockResolvedValueOnce({
955
- ok: true,
956
- json: async () => ({
957
- sessions: [
958
- {
959
- id: 'session-1',
960
- tenantId: 'tenant-1',
961
- workspaceId: 'workspace-1',
962
- taskId: 'task-1',
963
- baseImageAssetId: 'asset-image-1',
964
- title: 'Homepage review',
965
- globalInstruction: 'Review this screenshot',
966
- annotations: [],
967
- createdByActorId: 'user-1',
968
- updatedByActorId: 'user-1',
969
- createdAt: '2026-03-25T12:00:00.000Z',
970
- updatedAt: '2026-03-25T12:00:00.000Z',
971
- deletedAt: null,
972
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
973
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
974
- },
975
- ],
976
- }),
977
- });
978
- const requestedTarget = {
979
- taskId: 'task-1',
980
- taskReferenceLabel: 'T-101',
981
- assetId: 'asset-image-1',
982
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
983
- displayName: 'Homepage shot',
984
- };
985
- const { rerender } = render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, onContextChange: () => { } }));
986
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
987
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
988
- rerender(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, onContextChange: () => { } }));
989
- await waitFor(() => {
990
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
991
- });
992
- });
993
- it('reloads sessions when the same annotated target is explicitly reopened', async () => {
994
- fetchTaskforceApi
995
- .mockResolvedValueOnce({
996
- ok: true,
997
- json: async () => ({
998
- sessions: [
999
- {
1000
- id: 'session-1',
1001
- tenantId: 'tenant-1',
1002
- workspaceId: 'workspace-1',
1003
- taskId: 'task-1',
1004
- baseImageAssetId: 'asset-image-1',
1005
- title: 'Older review',
1006
- globalInstruction: 'Original session list',
1007
- annotations: [],
1008
- createdByActorId: 'user-1',
1009
- updatedByActorId: 'user-1',
1010
- createdAt: '2026-03-25T12:00:00.000Z',
1011
- updatedAt: '2026-03-25T12:00:00.000Z',
1012
- deletedAt: null,
1013
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1014
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1015
- },
1016
- ],
1017
- }),
1018
- })
1019
- .mockResolvedValueOnce({
1020
- ok: true,
1021
- json: async () => ({
1022
- sessions: [
1023
- {
1024
- id: 'session-2',
1025
- tenantId: 'tenant-1',
1026
- workspaceId: 'workspace-1',
1027
- taskId: 'task-1',
1028
- baseImageAssetId: 'asset-image-1',
1029
- title: 'Newest review',
1030
- globalInstruction: 'Freshly loaded session list',
1031
- annotations: [],
1032
- createdByActorId: 'user-2',
1033
- updatedByActorId: 'user-2',
1034
- createdAt: '2026-04-03T22:00:00.000Z',
1035
- updatedAt: '2026-04-03T22:00:00.000Z',
1036
- deletedAt: null,
1037
- createdByActor: { actorId: 'user-2', label: 'Jordan', type: 'user' },
1038
- updatedByActor: { actorId: 'user-2', label: 'Jordan', type: 'user' },
1039
- },
1040
- ],
1041
- }),
1042
- });
1043
- const requestedTarget = {
1044
- taskId: 'task-1',
1045
- taskReferenceLabel: 'T-101',
1046
- assetId: 'asset-image-1',
1047
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1048
- displayName: 'Homepage shot',
1049
- };
1050
- const { rerender } = render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, requestedOpenVersion: 1, onContextChange: () => { } }));
1051
- expect(await findSessionCard('Older review')).toBeInTheDocument();
1052
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(1);
1053
- rerender(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: requestedTarget, requestedOpenVersion: 2, onContextChange: () => { } }));
1054
- expect(await findSessionCard('Newest review')).toBeInTheDocument();
1055
- await waitFor(() => {
1056
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1057
- });
1058
- });
1059
- it('reloads sessions when the document becomes visible and the editor is not dirty', async () => {
1060
- fetchTaskforceApi
1061
- .mockResolvedValueOnce({
1062
- ok: true,
1063
- json: async () => ({
1064
- sessions: [
1065
- {
1066
- id: 'session-1',
1067
- tenantId: 'tenant-1',
1068
- workspaceId: 'workspace-1',
1069
- taskId: 'task-1',
1070
- baseImageAssetId: 'asset-image-1',
1071
- title: 'Homepage review',
1072
- globalInstruction: 'Review this screenshot',
1073
- annotations: [],
1074
- createdByActorId: 'user-1',
1075
- updatedByActorId: 'user-1',
1076
- createdAt: '2026-03-25T12:00:00.000Z',
1077
- updatedAt: '2026-03-25T12:00:00.000Z',
1078
- deletedAt: null,
1079
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1080
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1081
- },
1082
- ],
1083
- }),
1084
- })
1085
- .mockResolvedValueOnce({
1086
- ok: true,
1087
- json: async () => ({
1088
- sessions: [
1089
- {
1090
- id: 'session-2',
1091
- tenantId: 'tenant-1',
1092
- workspaceId: 'workspace-1',
1093
- taskId: 'task-1',
1094
- baseImageAssetId: 'asset-image-1',
1095
- title: 'Synced on visibility',
1096
- globalInstruction: 'Server-authoritative',
1097
- annotations: [],
1098
- createdByActorId: 'user-1',
1099
- updatedByActorId: 'user-1',
1100
- createdAt: '2026-03-25T12:05:00.000Z',
1101
- updatedAt: '2026-03-25T12:06:00.000Z',
1102
- deletedAt: null,
1103
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1104
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1105
- },
1106
- ],
1107
- }),
1108
- });
1109
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1110
- taskId: 'task-1',
1111
- taskReferenceLabel: 'T-101',
1112
- assetId: 'asset-image-1',
1113
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1114
- displayName: 'Homepage shot',
1115
- } }));
1116
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1117
- Object.defineProperty(document, 'visibilityState', {
1118
- configurable: true,
1119
- value: 'visible',
1120
- });
1121
- fireEvent(document, new Event('visibilitychange'));
1122
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&taskId=task-1&imageAssetId=asset-image-1', expect.objectContaining({
1123
- credentials: 'include',
1124
- headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1125
- }), ''));
1126
- expect(await findSessionCard('Synced on visibility')).toBeInTheDocument();
1127
- });
1128
- it('uses the provided task reference resolver when the launch target lacks a readable label', async () => {
1129
- const user = userEvent.setup();
1130
- const writeText = vi.fn().mockResolvedValue(undefined);
1131
- Object.defineProperty(navigator, 'clipboard', {
1132
- configurable: true,
1133
- value: { writeText },
1134
- });
1135
- mockAutoCreateFirstSession();
1136
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1137
- taskId: 'task-1',
1138
- assetId: 'asset-image-1',
1139
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1140
- displayName: 'Homepage shot',
1141
- }, resolveTaskReferenceLabel: () => 'T-101' }));
1142
- await findSessionCard('Homepage shot review');
1143
- expect(screen.getByText('T-101')).toBeInTheDocument();
1144
- await user.click(screen.getByRole('button', { name: /copy task id/i }));
1145
- expect(writeText).toHaveBeenCalledWith('T-101');
1146
- });
1147
- it('prefers the resolved T-XXX reference when the incoming label is just the raw task id', async () => {
1148
- const user = userEvent.setup();
1149
- const writeText = vi.fn().mockResolvedValue(undefined);
1150
- Object.defineProperty(navigator, 'clipboard', {
1151
- configurable: true,
1152
- value: { writeText },
1153
- });
1154
- mockAutoCreateFirstSession();
1155
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1156
- taskId: 'task-1',
1157
- taskReferenceLabel: 'task-1',
1158
- assetId: 'asset-image-1',
1159
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1160
- displayName: 'Homepage shot',
1161
- }, resolveTaskReferenceLabel: () => 'T-101' }));
1162
- await findSessionCard('Homepage shot review');
1163
- expect(screen.getByText('T-101')).toBeInTheDocument();
1164
- await user.click(screen.getByRole('button', { name: /copy task id/i }));
1165
- expect(writeText).toHaveBeenCalledWith('T-101');
1166
- });
1167
- it('uses the provided image reference resolver when the launch target lacks a readable image label', async () => {
1168
- const user = userEvent.setup();
1169
- const writeText = vi.fn().mockResolvedValue(undefined);
1170
- Object.defineProperty(navigator, 'clipboard', {
1171
- configurable: true,
1172
- value: { writeText },
1173
- });
1174
- mockAutoCreateFirstSession();
1175
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1176
- assetId: 'asset-image-1',
1177
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1178
- displayName: 'Standalone shot',
1179
- }, resolveImageReferenceLabel: () => 'I-22' }));
1180
- await findSessionCard('Homepage shot review');
1181
- expect(screen.getByText('I-22')).toBeInTheDocument();
1182
- await user.click(screen.getByRole('button', { name: /copy image reference/i }));
1183
- expect(writeText).toHaveBeenCalledWith('I-22');
1184
- expect(screen.getByRole('button', { name: /copied image reference/i })).toBeInTheDocument();
1185
- });
1186
- it('deletes the selected marker when the user presses Delete', async () => {
1187
- const user = userEvent.setup();
1188
- fetchTaskforceApi.mockResolvedValueOnce({
1189
- ok: true,
1190
- json: async () => ({
1191
- sessions: [
1192
- {
1193
- id: 'session-1',
1194
- tenantId: 'tenant-1',
1195
- workspaceId: 'workspace-1',
1196
- taskId: 'task-1',
1197
- baseImageAssetId: 'asset-image-1',
1198
- title: 'Homepage review',
1199
- globalInstruction: 'Review this screenshot',
1200
- annotations: [
1201
- {
1202
- id: 'annotation-1',
1203
- order: 0,
1204
- kind: 'pin',
1205
- x: 0.2,
1206
- y: 0.3,
1207
- instruction: 'Inspect this button',
1208
- markerType: 'question',
1209
- color: '#0f766e',
1210
- },
1211
- ],
1212
- createdByActorId: 'user-1',
1213
- updatedByActorId: 'user-1',
1214
- createdAt: '2026-03-25T12:00:00.000Z',
1215
- updatedAt: '2026-03-25T12:00:00.000Z',
1216
- deletedAt: null,
1217
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1218
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1219
- },
1220
- ],
1221
- }),
1222
- });
1223
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1224
- taskId: 'task-1',
1225
- taskReferenceLabel: 'T-101',
1226
- assetId: 'asset-image-1',
1227
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1228
- displayName: 'Homepage shot',
1229
- } }));
1230
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1231
- await user.click(screen.getByRole('button', { name: 'Pin' }));
1232
- await user.keyboard('{Delete}');
1233
- await waitFor(() => {
1234
- expect(screen.queryByRole('button', { name: 'Pin' })).not.toBeInTheDocument();
1235
- });
1236
- expect(screen.getByText('Add a marker on the image to begin.')).toBeInTheDocument();
1237
- });
1238
- it('does not delete the selected marker when Delete is pressed while typing in a field', async () => {
1239
- const user = userEvent.setup();
1240
- fetchTaskforceApi.mockResolvedValueOnce({
1241
- ok: true,
1242
- json: async () => ({
1243
- sessions: [
1244
- {
1245
- id: 'session-1',
1246
- tenantId: 'tenant-1',
1247
- workspaceId: 'workspace-1',
1248
- taskId: 'task-1',
1249
- baseImageAssetId: 'asset-image-1',
1250
- title: 'Homepage review',
1251
- globalInstruction: 'Review this screenshot',
1252
- annotations: [
1253
- {
1254
- id: 'annotation-1',
1255
- order: 0,
1256
- kind: 'pin',
1257
- x: 0.2,
1258
- y: 0.3,
1259
- instruction: 'Inspect this button',
1260
- markerType: 'question',
1261
- color: '#0f766e',
1262
- },
1263
- ],
1264
- createdByActorId: 'user-1',
1265
- updatedByActorId: 'user-1',
1266
- createdAt: '2026-03-25T12:00:00.000Z',
1267
- updatedAt: '2026-03-25T12:00:00.000Z',
1268
- deletedAt: null,
1269
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1270
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1271
- },
1272
- ],
1273
- }),
1274
- });
1275
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1276
- taskId: 'task-1',
1277
- taskReferenceLabel: 'T-101',
1278
- assetId: 'asset-image-1',
1279
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1280
- displayName: 'Homepage shot',
1281
- } }));
1282
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1283
- await user.click(screen.getByRole('button', { name: 'Pin' }));
1284
- const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
1285
- instructionField.focus();
1286
- fireEvent.keyDown(instructionField, { key: 'Delete' });
1287
- expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument();
1288
- });
1289
- it('duplicates and deletes sessions within the annotated attachment module shell', async () => {
1290
- const user = userEvent.setup();
1291
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
1292
- fetchTaskforceApi
1293
- .mockResolvedValueOnce({
1294
- ok: true,
1295
- json: async () => ({
1296
- sessions: [
1297
- {
1298
- id: 'session-1',
1299
- tenantId: 'tenant-1',
1300
- workspaceId: 'workspace-1',
1301
- taskId: 'task-1',
1302
- baseImageAssetId: 'asset-image-1',
1303
- title: 'Homepage review',
1304
- globalInstruction: 'Review this screenshot',
1305
- annotations: [
1306
- {
1307
- id: 'annotation-1',
1308
- order: 0,
1309
- kind: 'pin',
1310
- x: 0.2,
1311
- y: 0.3,
1312
- instruction: 'Inspect this button',
1313
- markerType: 'question',
1314
- color: '#0f766e',
1315
- },
1316
- ],
1317
- createdByActorId: 'user-1',
1318
- updatedByActorId: 'user-1',
1319
- createdAt: '2026-03-25T12:00:00.000Z',
1320
- updatedAt: '2026-03-25T12:00:00.000Z',
1321
- deletedAt: null,
1322
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1323
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1324
- },
1325
- ],
1326
- }),
1327
- })
1328
- .mockResolvedValueOnce({
1329
- ok: true,
1330
- json: async () => ({
1331
- success: true,
1332
- session: {
1333
- id: 'session-2',
1334
- tenantId: 'tenant-1',
1335
- workspaceId: 'workspace-1',
1336
- taskId: 'task-1',
1337
- baseImageAssetId: 'asset-image-1',
1338
- title: 'Homepage review copy',
1339
- globalInstruction: 'Review this screenshot',
1340
- annotations: [
1341
- {
1342
- id: 'annotation-copy-1',
1343
- order: 0,
1344
- kind: 'pin',
1345
- x: 0.2,
1346
- y: 0.3,
1347
- instruction: 'Inspect this button',
1348
- markerType: 'question',
1349
- color: '#0f766e',
1350
- },
1351
- ],
1352
- createdByActorId: 'user-1',
1353
- updatedByActorId: 'user-1',
1354
- createdAt: '2026-03-25T12:05:00.000Z',
1355
- updatedAt: '2026-03-25T12:05:00.000Z',
1356
- deletedAt: null,
1357
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1358
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1359
- },
1360
- }),
1361
- })
1362
- .mockResolvedValueOnce({
1363
- ok: true,
1364
- json: async () => ({
1365
- sessions: [
1366
- {
1367
- id: 'session-2',
1368
- tenantId: 'tenant-1',
1369
- workspaceId: 'workspace-1',
1370
- taskId: 'task-1',
1371
- baseImageAssetId: 'asset-image-1',
1372
- title: 'Homepage review copy',
1373
- globalInstruction: 'Review this screenshot',
1374
- annotations: [
1375
- {
1376
- id: 'annotation-copy-1',
1377
- order: 0,
1378
- kind: 'pin',
1379
- x: 0.2,
1380
- y: 0.3,
1381
- instruction: 'Inspect this button',
1382
- markerType: 'question',
1383
- color: '#0f766e',
1384
- },
1385
- ],
1386
- createdByActorId: 'user-1',
1387
- updatedByActorId: 'user-1',
1388
- createdAt: '2026-03-25T12:05:00.000Z',
1389
- updatedAt: '2026-03-25T12:05:00.000Z',
1390
- deletedAt: null,
1391
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1392
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1393
- },
1394
- {
1395
- id: 'session-1',
1396
- tenantId: 'tenant-1',
1397
- workspaceId: 'workspace-1',
1398
- taskId: 'task-1',
1399
- baseImageAssetId: 'asset-image-1',
1400
- title: 'Homepage review',
1401
- globalInstruction: 'Review this screenshot',
1402
- annotations: [
1403
- {
1404
- id: 'annotation-1',
1405
- order: 0,
1406
- kind: 'pin',
1407
- x: 0.2,
1408
- y: 0.3,
1409
- instruction: 'Inspect this button',
1410
- markerType: 'question',
1411
- color: '#0f766e',
1412
- },
1413
- ],
1414
- createdByActorId: 'user-1',
1415
- updatedByActorId: 'user-1',
1416
- createdAt: '2026-03-25T12:00:00.000Z',
1417
- updatedAt: '2026-03-25T12:00:00.000Z',
1418
- deletedAt: null,
1419
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1420
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1421
- },
1422
- ],
1423
- }),
1424
- })
1425
- .mockResolvedValueOnce({
1426
- ok: true,
1427
- json: async () => ({
1428
- session: {
1429
- id: 'session-2',
1430
- tenantId: 'tenant-1',
1431
- workspaceId: 'workspace-1',
1432
- taskId: 'task-1',
1433
- baseImageAssetId: 'asset-image-1',
1434
- title: 'Homepage review copy',
1435
- globalInstruction: 'Review this screenshot',
1436
- annotations: [
1437
- {
1438
- id: 'annotation-copy-1',
1439
- order: 0,
1440
- kind: 'pin',
1441
- x: 0.2,
1442
- y: 0.3,
1443
- instruction: 'Inspect this button',
1444
- markerType: 'question',
1445
- color: '#0f766e',
1446
- },
1447
- ],
1448
- createdByActorId: 'user-1',
1449
- updatedByActorId: 'user-1',
1450
- createdAt: '2026-03-25T12:04:00.000Z',
1451
- updatedAt: '2026-03-25T12:04:00.000Z',
1452
- deletedAt: null,
1453
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1454
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1455
- },
1456
- }),
1457
- })
1458
- .mockResolvedValueOnce({
1459
- ok: true,
1460
- json: async () => ({ success: true }),
1461
- });
1462
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1463
- taskId: 'task-1',
1464
- taskReferenceLabel: 'T-101',
1465
- assetId: 'asset-image-1',
1466
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1467
- displayName: 'Homepage shot',
1468
- } }));
1469
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(1, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1&taskId=task-1&imageAssetId=asset-image-1', expect.objectContaining({
1470
- credentials: 'include',
1471
- headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1472
- }), ''));
1473
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1474
- await user.click(screen.getByRole('button', { name: /duplicate session/i }));
1475
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({ method: 'POST' }), ''));
1476
- const duplicateBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1477
- expect(duplicateBody.title).toBe('Homepage review copy');
1478
- expect(duplicateBody.annotations).toHaveLength(1);
1479
- expect(duplicateBody.annotations[0]?.instruction).toBe('Inspect this button');
1480
- expect(await findSessionCard('Homepage review copy')).toBeInTheDocument();
1481
- await user.click(screen.getByRole('button', { name: /delete session/i }));
1482
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-2?workspaceId=workspace-1', expect.objectContaining({ method: 'DELETE' }), ''));
1483
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(3);
1484
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1485
- expect(confirmSpy).toHaveBeenCalledWith('Delete the annotated attachment session "Homepage review copy"?');
1486
- });
1487
- it('auto-creates the first session and uses the created session immediately on entry', async () => {
1488
- fetchTaskforceApi
1489
- .mockResolvedValueOnce({
1490
- ok: true,
1491
- json: async () => ({
1492
- sessions: [],
1493
- }),
1494
- })
1495
- .mockResolvedValueOnce({
1496
- ok: true,
1497
- json: async () => ({
1498
- success: true,
1499
- session: {
1500
- id: 'session-2',
1501
- tenantId: 'tenant-1',
1502
- workspaceId: 'workspace-1',
1503
- taskId: 'task-1',
1504
- baseImageAssetId: 'asset-image-1',
1505
- title: 'Homepage shot notes',
1506
- globalInstruction: '',
1507
- annotations: [],
1508
- createdByActorId: 'user-1',
1509
- updatedByActorId: 'user-1',
1510
- createdAt: '2026-03-25T12:05:00.000Z',
1511
- updatedAt: '2026-03-25T12:05:00.000Z',
1512
- deletedAt: null,
1513
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1514
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1515
- },
1516
- }),
1517
- });
1518
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1519
- taskId: 'task-1',
1520
- taskReferenceLabel: 'T-101',
1521
- assetId: 'asset-image-1',
1522
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1523
- displayName: 'Homepage shot',
1524
- } }));
1525
- expect(await findSessionCard('Homepage shot notes')).toBeInTheDocument();
1526
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions?workspaceId=workspace-1', expect.objectContaining({ method: 'POST' }), ''));
1527
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1528
- expect(screen.getByText('Created by Alex')).toBeInTheDocument();
1529
- });
1530
- it('reorders markers and saves the updated annotation order', async () => {
1531
- const user = userEvent.setup();
1532
- fetchTaskforceApi
1533
- .mockResolvedValueOnce({
1534
- ok: true,
1535
- json: async () => ({
1536
- sessions: [
1537
- {
1538
- id: 'session-1',
1539
- tenantId: 'tenant-1',
1540
- workspaceId: 'workspace-1',
1541
- taskId: 'task-1',
1542
- baseImageAssetId: 'asset-image-1',
1543
- title: 'Homepage review',
1544
- globalInstruction: 'Review this screenshot',
1545
- annotations: [
1546
- {
1547
- id: 'annotation-1',
1548
- order: 0,
1549
- kind: 'pin',
1550
- x: 0.2,
1551
- y: 0.3,
1552
- instruction: 'Inspect this button',
1553
- markerType: 'question',
1554
- color: '#0f766e',
1555
- },
1556
- {
1557
- id: 'annotation-2',
1558
- order: 1,
1559
- kind: 'box',
1560
- x: 0.4,
1561
- y: 0.2,
1562
- width: 0.2,
1563
- height: 0.2,
1564
- instruction: 'Review this card',
1565
- markerType: 'review',
1566
- color: '#7c3aed',
1567
- },
1568
- ],
1569
- createdByActorId: 'user-1',
1570
- updatedByActorId: 'user-1',
1571
- createdAt: '2026-03-25T12:00:00.000Z',
1572
- updatedAt: '2026-03-25T12:00:00.000Z',
1573
- deletedAt: null,
1574
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1575
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1576
- },
1577
- ],
1578
- }),
1579
- })
1580
- .mockResolvedValueOnce({
1581
- ok: true,
1582
- json: async () => ({
1583
- success: true,
1584
- session: {
1585
- id: 'session-1',
1586
- tenantId: 'tenant-1',
1587
- workspaceId: 'workspace-1',
1588
- taskId: 'task-1',
1589
- baseImageAssetId: 'asset-image-1',
1590
- title: 'Homepage review',
1591
- globalInstruction: 'Review this screenshot',
1592
- annotations: [
1593
- {
1594
- id: 'annotation-2',
1595
- order: 0,
1596
- kind: 'box',
1597
- x: 0.4,
1598
- y: 0.2,
1599
- width: 0.2,
1600
- height: 0.2,
1601
- instruction: 'Review this card',
1602
- markerType: 'review',
1603
- color: '#7c3aed',
1604
- },
1605
- {
1606
- id: 'annotation-1',
1607
- order: 1,
1608
- kind: 'pin',
1609
- x: 0.2,
1610
- y: 0.3,
1611
- instruction: 'Inspect this button',
1612
- markerType: 'question',
1613
- color: '#0f766e',
1614
- },
1615
- ],
1616
- createdByActorId: 'user-1',
1617
- updatedByActorId: 'user-1',
1618
- createdAt: '2026-03-25T12:00:00.000Z',
1619
- updatedAt: '2026-03-25T12:10:00.000Z',
1620
- deletedAt: null,
1621
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1622
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1623
- },
1624
- }),
1625
- });
1626
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1627
- taskId: 'task-1',
1628
- taskReferenceLabel: 'T-101',
1629
- assetId: 'asset-image-1',
1630
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1631
- displayName: 'Homepage shot',
1632
- } }));
1633
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1634
- expect(screen.getByRole('button', { name: 'Pin' })).toBeInTheDocument();
1635
- await user.click(screen.getByRole('button', { name: /move marker down/i }));
1636
- expect(screen.getByRole('button', { name: 'Box' })).toBeInTheDocument();
1637
- await user.click(screen.getByRole('button', { name: /save session/i }));
1638
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1639
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1640
- expect(saveBody.annotations.map((annotation) => annotation.kind)).toEqual(['box', 'pin']);
1641
- expect(fetchTaskforceApi).toHaveBeenCalledTimes(2);
1642
- expect(await screen.findByRole('button', { name: 'Box' })).toBeInTheDocument();
1643
- });
1644
- it('loads an AI payload preview for the selected session', async () => {
1645
- const user = userEvent.setup();
1646
- fetchTaskforceApi
1647
- .mockResolvedValueOnce({
1648
- ok: true,
1649
- json: async () => ({
1650
- sessions: [
1651
- {
1652
- id: 'session-1',
1653
- tenantId: 'tenant-1',
1654
- workspaceId: 'workspace-1',
1655
- taskId: 'task-1',
1656
- baseImageAssetId: 'asset-image-1',
1657
- title: 'Homepage review',
1658
- globalInstruction: 'Review this screenshot',
1659
- annotations: [
1660
- {
1661
- id: 'annotation-1',
1662
- order: 0,
1663
- kind: 'pin',
1664
- x: 0.2,
1665
- y: 0.3,
1666
- instruction: 'Inspect this button',
1667
- markerType: 'question',
1668
- color: '#0f766e',
1669
- },
1670
- ],
1671
- createdByActorId: 'user-1',
1672
- updatedByActorId: 'user-1',
1673
- createdAt: '2026-03-25T12:00:00.000Z',
1674
- updatedAt: '2026-03-25T12:00:00.000Z',
1675
- deletedAt: null,
1676
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1677
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1678
- },
1679
- ],
1680
- }),
1681
- })
1682
- .mockResolvedValueOnce({
1683
- ok: true,
1684
- json: async () => ({
1685
- payload: {
1686
- taskId: 'task-1',
1687
- workspaceId: 'workspace-1',
1688
- sessionId: 'session-1',
1689
- title: 'Homepage review',
1690
- globalInstruction: 'Review this screenshot',
1691
- image: {
1692
- assetId: 'asset-image-1',
1693
- storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
1694
- apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1695
- mimeType: 'image/png',
1696
- originalFilename: 'homepage.png',
1697
- logicalName: 'homepage',
1698
- displayName: 'Homepage shot',
1699
- sizeBytes: 1024,
1700
- updatedAt: '2026-03-25T12:00:00.000Z',
1701
- },
1702
- annotations: [
1703
- {
1704
- id: 'annotation-1',
1705
- order: 0,
1706
- kind: 'pin',
1707
- x: 0.2,
1708
- y: 0.3,
1709
- instruction: 'Inspect this button',
1710
- markerType: 'question',
1711
- color: '#0f766e',
1712
- cropHint: {
1713
- x: 0.11,
1714
- y: 0.21,
1715
- width: 0.18,
1716
- height: 0.18,
1717
- },
1718
- },
1719
- ],
1720
- },
1721
- }),
1722
- });
1723
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1724
- taskId: 'task-1',
1725
- taskReferenceLabel: 'T-101',
1726
- assetId: 'asset-image-1',
1727
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1728
- displayName: 'Homepage shot',
1729
- } }));
1730
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1731
- await user.click(screen.getByRole('button', { name: /preview payload/i }));
1732
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1/payload?workspaceId=workspace-1', expect.objectContaining({
1733
- credentials: 'include',
1734
- headers: { 'x-taskforce-workspace-id': 'workspace-1' },
1735
- }), ''));
1736
- expect(await screen.findByText(/"sessionId": "session-1"/)).toBeInTheDocument();
1737
- expect(screen.getByText(/"cropHint"/)).toBeInTheDocument();
1738
- });
1739
- it('updates marker geometry fields and saves the edited coordinates', async () => {
1740
- const user = userEvent.setup();
1741
- fetchTaskforceApi
1742
- .mockResolvedValueOnce({
1743
- ok: true,
1744
- json: async () => ({
1745
- sessions: [
1746
- {
1747
- id: 'session-1',
1748
- tenantId: 'tenant-1',
1749
- workspaceId: 'workspace-1',
1750
- taskId: 'task-1',
1751
- baseImageAssetId: 'asset-image-1',
1752
- title: 'Homepage review',
1753
- globalInstruction: 'Review this screenshot',
1754
- annotations: [
1755
- {
1756
- id: 'annotation-1',
1757
- order: 0,
1758
- kind: 'box',
1759
- x: 0.4,
1760
- y: 0.2,
1761
- width: 0.2,
1762
- height: 0.2,
1763
- instruction: 'Review this card',
1764
- markerType: 'review',
1765
- color: '#7c3aed',
1766
- },
1767
- ],
1768
- createdByActorId: 'user-1',
1769
- updatedByActorId: 'user-1',
1770
- createdAt: '2026-03-25T12:00:00.000Z',
1771
- updatedAt: '2026-03-25T12:00:00.000Z',
1772
- deletedAt: null,
1773
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1774
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1775
- },
1776
- ],
1777
- }),
1778
- })
1779
- .mockResolvedValueOnce({
1780
- ok: true,
1781
- json: async () => ({
1782
- success: true,
1783
- session: {
1784
- id: 'session-1',
1785
- tenantId: 'tenant-1',
1786
- workspaceId: 'workspace-1',
1787
- taskId: 'task-1',
1788
- baseImageAssetId: 'asset-image-1',
1789
- title: 'Homepage review',
1790
- globalInstruction: 'Review this screenshot',
1791
- annotations: [
1792
- {
1793
- id: 'annotation-1',
1794
- order: 0,
1795
- kind: 'box',
1796
- x: 0.4,
1797
- y: 0.2,
1798
- width: 0.35,
1799
- height: 0.25,
1800
- instruction: 'Review this card',
1801
- markerType: 'review',
1802
- color: '#7c3aed',
1803
- },
1804
- ],
1805
- createdByActorId: 'user-1',
1806
- updatedByActorId: 'user-1',
1807
- createdAt: '2026-03-25T12:00:00.000Z',
1808
- updatedAt: '2026-03-25T12:15:00.000Z',
1809
- deletedAt: null,
1810
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1811
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1812
- },
1813
- }),
1814
- });
1815
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1816
- taskId: 'task-1',
1817
- taskReferenceLabel: 'T-101',
1818
- assetId: 'asset-image-1',
1819
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1820
- displayName: 'Homepage shot',
1821
- } }));
1822
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1823
- const widthInput = screen.getByRole('spinbutton', { name: /width percent/i });
1824
- const heightInput = screen.getByRole('spinbutton', { name: /height percent/i });
1825
- fireEvent.change(widthInput, { target: { value: '35' } });
1826
- fireEvent.change(heightInput, { target: { value: '25' } });
1827
- await user.click(screen.getByRole('button', { name: /save session/i }));
1828
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1829
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1830
- expect(saveBody.annotations[0]?.width).toBe(0.35);
1831
- expect(saveBody.annotations[0]?.height).toBe(0.25);
1832
- });
1833
- it('selects and drags an arrow marker from the canvas hit area', async () => {
1834
- const user = userEvent.setup();
1835
- fetchTaskforceApi
1836
- .mockResolvedValueOnce({
1837
- ok: true,
1838
- json: async () => ({
1839
- sessions: [
1840
- {
1841
- id: 'session-1',
1842
- tenantId: 'tenant-1',
1843
- workspaceId: 'workspace-1',
1844
- taskId: 'task-1',
1845
- baseImageAssetId: 'asset-image-1',
1846
- title: 'Homepage review',
1847
- globalInstruction: 'Review this screenshot',
1848
- annotations: [
1849
- {
1850
- id: 'annotation-pin',
1851
- order: 0,
1852
- kind: 'pin',
1853
- x: 0.15,
1854
- y: 0.2,
1855
- instruction: 'Inspect this button',
1856
- markerType: 'question',
1857
- color: '#0f766e',
1858
- },
1859
- {
1860
- id: 'annotation-arrow',
1861
- order: 1,
1862
- kind: 'arrow',
1863
- x: 0.2,
1864
- y: 0.3,
1865
- x2: 0.4,
1866
- y2: 0.5,
1867
- instruction: 'Follow this flow',
1868
- markerType: 'change',
1869
- color: '#2563eb',
1870
- },
1871
- ],
1872
- createdByActorId: 'user-1',
1873
- updatedByActorId: 'user-1',
1874
- createdAt: '2026-03-25T12:00:00.000Z',
1875
- updatedAt: '2026-03-25T12:00:00.000Z',
1876
- deletedAt: null,
1877
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1878
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1879
- },
1880
- ],
1881
- }),
1882
- })
1883
- .mockResolvedValueOnce({
1884
- ok: true,
1885
- json: async () => ({
1886
- success: true,
1887
- session: {
1888
- id: 'session-1',
1889
- tenantId: 'tenant-1',
1890
- workspaceId: 'workspace-1',
1891
- taskId: 'task-1',
1892
- baseImageAssetId: 'asset-image-1',
1893
- title: 'Homepage review',
1894
- globalInstruction: 'Review this screenshot',
1895
- annotations: [
1896
- {
1897
- id: 'annotation-pin',
1898
- order: 0,
1899
- kind: 'pin',
1900
- x: 0.15,
1901
- y: 0.2,
1902
- instruction: 'Inspect this button',
1903
- markerType: 'question',
1904
- color: '#0f766e',
1905
- },
1906
- {
1907
- id: 'annotation-arrow',
1908
- order: 1,
1909
- kind: 'arrow',
1910
- x: 0.3,
1911
- y: 0.4,
1912
- x2: 0.5,
1913
- y2: 0.6,
1914
- instruction: 'Follow this flow',
1915
- markerType: 'change',
1916
- color: '#2563eb',
1917
- },
1918
- ],
1919
- createdByActorId: 'user-1',
1920
- updatedByActorId: 'user-1',
1921
- createdAt: '2026-03-25T12:00:00.000Z',
1922
- updatedAt: '2026-03-25T12:18:00.000Z',
1923
- deletedAt: null,
1924
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1925
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
1926
- },
1927
- }),
1928
- });
1929
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
1930
- taskId: 'task-1',
1931
- assetId: 'asset-image-1',
1932
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
1933
- displayName: 'Homepage shot',
1934
- } }));
1935
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
1936
- fireEvent.load(screen.getByAltText('Homepage shot'));
1937
- const overlay = screen.getByTestId('annotated-attachment-overlay');
1938
- Object.defineProperty(overlay, 'getBoundingClientRect', {
1939
- configurable: true,
1940
- value: () => ({
1941
- left: 0,
1942
- top: 0,
1943
- width: 100,
1944
- height: 100,
1945
- right: 100,
1946
- bottom: 100,
1947
- x: 0,
1948
- y: 0,
1949
- toJSON: () => ({}),
1950
- }),
1951
- });
1952
- const arrowHitArea = screen.getByTestId('annotated-arrow-hit-annotation-arrow');
1953
- fireEvent.pointerDown(arrowHitArea, { clientX: 20, clientY: 30 });
1954
- fireEvent.pointerMove(overlay, { clientX: 30, clientY: 40 });
1955
- fireEvent.pointerUp(overlay, { clientX: 30, clientY: 40 });
1956
- expect(screen.getByDisplayValue('Follow this flow')).toBeInTheDocument();
1957
- await user.click(screen.getByRole('button', { name: /save session/i }));
1958
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
1959
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
1960
- expect(saveBody.annotations[1]?.x).toBeCloseTo(0.3, 6);
1961
- expect(saveBody.annotations[1]?.y).toBeCloseTo(0.4, 6);
1962
- expect(saveBody.annotations[1]?.x2).toBeCloseTo(0.5, 6);
1963
- expect(saveBody.annotations[1]?.y2).toBeCloseTo(0.6, 6);
1964
- });
1965
- it('resizes a selected box marker from the canvas handle', async () => {
1966
- const user = userEvent.setup();
1967
- fetchTaskforceApi
1968
- .mockResolvedValueOnce({
1969
- ok: true,
1970
- json: async () => ({
1971
- sessions: [
1972
- {
1973
- id: 'session-1',
1974
- tenantId: 'tenant-1',
1975
- workspaceId: 'workspace-1',
1976
- taskId: 'task-1',
1977
- baseImageAssetId: 'asset-image-1',
1978
- title: 'Homepage review',
1979
- globalInstruction: 'Review this screenshot',
1980
- annotations: [
1981
- {
1982
- id: 'annotation-box',
1983
- order: 0,
1984
- kind: 'box',
1985
- x: 0.2,
1986
- y: 0.25,
1987
- width: 0.2,
1988
- height: 0.2,
1989
- instruction: 'Resize this region',
1990
- markerType: 'review',
1991
- color: '#7c3aed',
1992
- },
1993
- ],
1994
- createdByActorId: 'user-1',
1995
- updatedByActorId: 'user-1',
1996
- createdAt: '2026-03-25T12:00:00.000Z',
1997
- updatedAt: '2026-03-25T12:00:00.000Z',
1998
- deletedAt: null,
1999
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2000
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2001
- },
2002
- ],
2003
- }),
2004
- })
2005
- .mockResolvedValueOnce({
2006
- ok: true,
2007
- json: async () => ({
2008
- success: true,
2009
- session: {
2010
- id: 'session-1',
2011
- tenantId: 'tenant-1',
2012
- workspaceId: 'workspace-1',
2013
- taskId: 'task-1',
2014
- baseImageAssetId: 'asset-image-1',
2015
- title: 'Homepage review',
2016
- globalInstruction: 'Review this screenshot',
2017
- annotations: [
2018
- {
2019
- id: 'annotation-box',
2020
- order: 0,
2021
- kind: 'box',
2022
- x: 0.2,
2023
- y: 0.25,
2024
- width: 0.3,
2025
- height: 0.3,
2026
- instruction: 'Resize this region',
2027
- markerType: 'review',
2028
- color: '#7c3aed',
2029
- },
2030
- ],
2031
- createdByActorId: 'user-1',
2032
- updatedByActorId: 'user-1',
2033
- createdAt: '2026-03-25T12:00:00.000Z',
2034
- updatedAt: '2026-03-25T12:21:00.000Z',
2035
- deletedAt: null,
2036
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2037
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2038
- },
2039
- }),
2040
- });
2041
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2042
- taskId: 'task-1',
2043
- assetId: 'asset-image-1',
2044
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2045
- displayName: 'Homepage shot',
2046
- } }));
2047
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2048
- fireEvent.load(screen.getByAltText('Homepage shot'));
2049
- const overlay = screen.getByTestId('annotated-attachment-overlay');
2050
- Object.defineProperty(overlay, 'getBoundingClientRect', {
2051
- configurable: true,
2052
- value: () => ({
2053
- left: 0,
2054
- top: 0,
2055
- width: 100,
2056
- height: 100,
2057
- right: 100,
2058
- bottom: 100,
2059
- x: 0,
2060
- y: 0,
2061
- toJSON: () => ({}),
2062
- }),
2063
- });
2064
- await user.click(screen.getByRole('button', { name: 'Box' }));
2065
- const resizeHandle = screen.getByRole('button', { name: /resize box marker/i });
2066
- fireEvent.pointerDown(resizeHandle, { clientX: 40, clientY: 45 });
2067
- fireEvent.pointerMove(overlay, { clientX: 50, clientY: 55 });
2068
- fireEvent.pointerUp(overlay, { clientX: 50, clientY: 55 });
2069
- await user.click(screen.getByRole('button', { name: /save session/i }));
2070
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2071
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2072
- expect(saveBody.annotations[0]?.width).toBeCloseTo(0.3, 6);
2073
- expect(saveBody.annotations[0]?.height).toBeCloseTo(0.3, 6);
2074
- });
2075
- it('moves the selected arrow end handle on the canvas', async () => {
2076
- const user = userEvent.setup();
2077
- fetchTaskforceApi
2078
- .mockResolvedValueOnce({
2079
- ok: true,
2080
- json: async () => ({
2081
- sessions: [
2082
- {
2083
- id: 'session-1',
2084
- tenantId: 'tenant-1',
2085
- workspaceId: 'workspace-1',
2086
- taskId: 'task-1',
2087
- baseImageAssetId: 'asset-image-1',
2088
- title: 'Homepage review',
2089
- globalInstruction: 'Review this screenshot',
2090
- annotations: [
2091
- {
2092
- id: 'annotation-arrow',
2093
- order: 0,
2094
- kind: 'arrow',
2095
- x: 0.2,
2096
- y: 0.3,
2097
- x2: 0.4,
2098
- y2: 0.5,
2099
- instruction: 'Move this endpoint',
2100
- markerType: 'change',
2101
- color: '#2563eb',
2102
- },
2103
- ],
2104
- createdByActorId: 'user-1',
2105
- updatedByActorId: 'user-1',
2106
- createdAt: '2026-03-25T12:00:00.000Z',
2107
- updatedAt: '2026-03-25T12:00:00.000Z',
2108
- deletedAt: null,
2109
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2110
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2111
- },
2112
- ],
2113
- }),
2114
- })
2115
- .mockResolvedValueOnce({
2116
- ok: true,
2117
- json: async () => ({
2118
- success: true,
2119
- session: {
2120
- id: 'session-1',
2121
- tenantId: 'tenant-1',
2122
- workspaceId: 'workspace-1',
2123
- taskId: 'task-1',
2124
- baseImageAssetId: 'asset-image-1',
2125
- title: 'Homepage review',
2126
- globalInstruction: 'Review this screenshot',
2127
- annotations: [
2128
- {
2129
- id: 'annotation-arrow',
2130
- order: 0,
2131
- kind: 'arrow',
2132
- x: 0.2,
2133
- y: 0.3,
2134
- x2: 0.55,
2135
- y2: 0.65,
2136
- instruction: 'Move this endpoint',
2137
- markerType: 'change',
2138
- color: '#2563eb',
2139
- },
2140
- ],
2141
- createdByActorId: 'user-1',
2142
- updatedByActorId: 'user-1',
2143
- createdAt: '2026-03-25T12:00:00.000Z',
2144
- updatedAt: '2026-03-25T12:24:00.000Z',
2145
- deletedAt: null,
2146
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2147
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2148
- },
2149
- }),
2150
- });
2151
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2152
- taskId: 'task-1',
2153
- assetId: 'asset-image-1',
2154
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2155
- displayName: 'Homepage shot',
2156
- } }));
2157
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2158
- fireEvent.load(screen.getByAltText('Homepage shot'));
2159
- const overlay = screen.getByTestId('annotated-attachment-overlay');
2160
- Object.defineProperty(overlay, 'getBoundingClientRect', {
2161
- configurable: true,
2162
- value: () => ({
2163
- left: 0,
2164
- top: 0,
2165
- width: 100,
2166
- height: 100,
2167
- right: 100,
2168
- bottom: 100,
2169
- x: 0,
2170
- y: 0,
2171
- toJSON: () => ({}),
2172
- }),
2173
- });
2174
- await user.click(screen.getByRole('button', { name: 'Arrow' }));
2175
- const endHandle = screen.getByRole('button', { name: /move arrow tip/i });
2176
- fireEvent.pointerDown(endHandle, { clientX: 40, clientY: 50 });
2177
- fireEvent.pointerMove(overlay, { clientX: 55, clientY: 65 });
2178
- fireEvent.pointerUp(overlay, { clientX: 55, clientY: 65 });
2179
- await user.click(screen.getByRole('button', { name: /save session/i }));
2180
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2181
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2182
- expect(saveBody.annotations[0]?.x2).toBeCloseTo(0.55, 6);
2183
- expect(saveBody.annotations[0]?.y2).toBeCloseTo(0.65, 6);
2184
- });
2185
- it('copies payload JSON and an AI brief from the payload preview', async () => {
2186
- const user = userEvent.setup();
2187
- const writeText = vi.fn().mockResolvedValue(undefined);
2188
- Object.defineProperty(navigator, 'clipboard', {
2189
- configurable: true,
2190
- value: { writeText },
2191
- });
2192
- fetchTaskforceApi
2193
- .mockResolvedValueOnce({
2194
- ok: true,
2195
- json: async () => ({
2196
- sessions: [
2197
- {
2198
- id: 'session-1',
2199
- tenantId: 'tenant-1',
2200
- workspaceId: 'workspace-1',
2201
- taskId: 'task-1',
2202
- baseImageAssetId: 'asset-image-1',
2203
- title: 'Homepage review',
2204
- globalInstruction: 'Review this screenshot',
2205
- annotations: [
2206
- {
2207
- id: 'annotation-1',
2208
- order: 0,
2209
- kind: 'pin',
2210
- x: 0.2,
2211
- y: 0.3,
2212
- instruction: 'Inspect this button',
2213
- markerType: 'question',
2214
- color: '#0f766e',
2215
- },
2216
- ],
2217
- createdByActorId: 'user-1',
2218
- updatedByActorId: 'user-1',
2219
- createdAt: '2026-03-25T12:00:00.000Z',
2220
- updatedAt: '2026-03-25T12:00:00.000Z',
2221
- deletedAt: null,
2222
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2223
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2224
- },
2225
- ],
2226
- }),
2227
- })
2228
- .mockResolvedValueOnce({
2229
- ok: true,
2230
- json: async () => ({
2231
- payload: {
2232
- taskId: 'task-1',
2233
- workspaceId: 'workspace-1',
2234
- sessionId: 'session-1',
2235
- title: 'Homepage review',
2236
- globalInstruction: 'Review this screenshot',
2237
- image: {
2238
- assetId: 'asset-image-1',
2239
- storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
2240
- apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2241
- mimeType: 'image/png',
2242
- originalFilename: 'homepage.png',
2243
- logicalName: 'homepage',
2244
- displayName: 'Homepage shot',
2245
- sizeBytes: 1024,
2246
- updatedAt: '2026-03-25T12:00:00.000Z',
2247
- },
2248
- annotations: [
2249
- {
2250
- id: 'annotation-1',
2251
- order: 0,
2252
- kind: 'pin',
2253
- x: 0.2,
2254
- y: 0.3,
2255
- instruction: 'Inspect this button',
2256
- markerType: 'question',
2257
- color: '#0f766e',
2258
- cropHint: {
2259
- x: 0.11,
2260
- y: 0.21,
2261
- width: 0.18,
2262
- height: 0.18,
2263
- },
2264
- },
2265
- ],
2266
- },
2267
- }),
2268
- });
2269
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2270
- taskId: 'task-1',
2271
- assetId: 'asset-image-1',
2272
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2273
- displayName: 'Homepage shot',
2274
- } }));
2275
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2276
- await user.click(screen.getByRole('button', { name: /preview payload/i }));
2277
- expect(await screen.findByText(/"sessionId": "session-1"/)).toBeInTheDocument();
2278
- await user.click(screen.getByRole('button', { name: /copy json/i }));
2279
- expect(writeText).toHaveBeenNthCalledWith(1, expect.stringContaining('"sessionId": "session-1"'));
2280
- await user.click(screen.getByRole('button', { name: /copy ai brief/i }));
2281
- expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('Annotated attachment: Homepage review'));
2282
- expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('1. pin (question)'));
2283
- expect(writeText).toHaveBeenNthCalledWith(2, expect.stringContaining('Instruction: Inspect this button'));
2284
- });
2285
- it('shows a back to task button when launched from a task and calls back with the task id', async () => {
2286
- const user = userEvent.setup();
2287
- const onBackToTask = vi.fn();
2288
- mockAutoCreateFirstSession();
2289
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2290
- taskId: 'task-1',
2291
- assetId: 'asset-image-1',
2292
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2293
- displayName: 'Homepage shot',
2294
- }, onBackToTask: onBackToTask }));
2295
- await findSessionCard('Homepage shot review');
2296
- await user.click(screen.getByRole('button', { name: /back to task/i }));
2297
- expect(onBackToTask).toHaveBeenCalledWith('task-1');
2298
- });
2299
- it('shows an unattached image label when the requested target is not linked to a task', async () => {
2300
- fetchTaskforceApi.mockResolvedValueOnce({
2301
- ok: true,
2302
- json: async () => ({
2303
- sessions: [],
2304
- }),
2305
- });
2306
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2307
- taskId: '',
2308
- assetId: 'asset-image-1',
2309
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2310
- displayName: 'Loose image',
2311
- } }));
2312
- await screen.findByText('No sessions yet');
2313
- expect(screen.getByText('Unattached Image')).toBeInTheDocument();
2314
- expect(screen.queryByRole('button', { name: /copy task id/i })).not.toBeInTheDocument();
2315
- });
2316
- it('does not repeat the image name in the sessions header', async () => {
2317
- mockAutoCreateFirstSession({ title: 'Image Notes review' });
2318
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2319
- taskId: 'task-1',
2320
- taskReferenceLabel: 'T-101',
2321
- assetId: 'asset-image-1',
2322
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2323
- displayName: 'Image Notes',
2324
- } }));
2325
- await findSessionCard('Image Notes review');
2326
- expect(screen.getByText('Image Notes')).toBeInTheDocument();
2327
- expect(screen.queryByText('Create and switch between annotation sessions.')).not.toBeInTheDocument();
2328
- });
2329
- it('restores a requested saved session and reports the active context', async () => {
2330
- const onContextChange = vi.fn();
2331
- fetchTaskforceApi.mockResolvedValueOnce({
2332
- ok: true,
2333
- json: async () => ({
2334
- sessions: [
2335
- {
2336
- id: 'session-a',
2337
- tenantId: 'tenant-1',
2338
- workspaceId: 'workspace-1',
2339
- taskId: 'task-1',
2340
- baseImageAssetId: 'asset-image-1',
2341
- title: 'First session',
2342
- globalInstruction: null,
2343
- annotations: [],
2344
- createdByActorId: 'user-1',
2345
- updatedByActorId: 'user-1',
2346
- createdAt: '2026-03-25T12:00:00.000Z',
2347
- updatedAt: '2026-03-25T12:00:00.000Z',
2348
- deletedAt: null,
2349
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2350
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2351
- },
2352
- {
2353
- id: 'session-b',
2354
- tenantId: 'tenant-1',
2355
- workspaceId: 'workspace-1',
2356
- taskId: 'task-1',
2357
- baseImageAssetId: 'asset-image-1',
2358
- title: 'Restored session',
2359
- globalInstruction: 'Resume here',
2360
- annotations: [],
2361
- createdByActorId: 'user-1',
2362
- updatedByActorId: 'user-1',
2363
- createdAt: '2026-03-25T12:05:00.000Z',
2364
- updatedAt: '2026-03-25T12:05:00.000Z',
2365
- deletedAt: null,
2366
- createdByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2367
- updatedByActor: { actorId: 'user-1', label: 'Alex', type: 'user' },
2368
- }
2369
- ],
2370
- }),
2371
- });
2372
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2373
- taskId: 'task-1',
2374
- assetId: 'asset-image-1',
2375
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2376
- displayName: 'Homepage shot',
2377
- }, requestedSessionId: "session-b", onContextChange: onContextChange }));
2378
- expect(await findSessionCard('Restored session')).toBeInTheDocument();
2379
- await waitFor(() => {
2380
- expect(onContextChange).toHaveBeenLastCalledWith(expect.objectContaining({
2381
- target: expect.objectContaining({
2382
- taskId: 'task-1',
2383
- assetId: 'asset-image-1',
2384
- }),
2385
- sessionId: 'session-b',
2386
- }));
2387
- });
2388
- });
2389
- it('warns when a requested saved session cannot be restored', async () => {
2390
- fetchTaskforceApi
2391
- .mockResolvedValueOnce({
2392
- ok: true,
2393
- json: async () => ({
2394
- sessions: [],
2395
- }),
2396
- })
2397
- .mockResolvedValueOnce({
2398
- ok: true,
2399
- json: async () => ({
2400
- session: buildSessionResponse({
2401
- id: 'session-new',
2402
- taskId: '',
2403
- baseImageAssetId: 'asset-image-1',
2404
- title: 'Recovered session',
2405
- }),
2406
- }),
2407
- });
2408
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2409
- taskId: '',
2410
- assetId: 'asset-image-1',
2411
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2412
- displayName: 'Homepage shot',
2413
- }, requestedSessionId: "missing-session" }));
2414
- expect(await screen.findByText('The previously selected annotation session could not be restored.')).toBeInTheDocument();
2415
- expect(await findSessionCard('Recovered session')).toBeInTheDocument();
2416
- });
2417
- it('copies the originating task id from the annotator header', async () => {
2418
- const user = userEvent.setup();
2419
- const writeText = vi.fn().mockResolvedValue(undefined);
2420
- Object.defineProperty(navigator, 'clipboard', {
2421
- configurable: true,
2422
- value: { writeText },
2423
- });
2424
- mockAutoCreateFirstSession();
2425
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2426
- taskId: 'task-1',
2427
- taskReferenceLabel: 'T-101',
2428
- assetId: 'asset-image-1',
2429
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2430
- displayName: 'Homepage shot',
2431
- } }));
2432
- await findSessionCard('Homepage shot review');
2433
- await user.click(screen.getByRole('button', { name: /copy task id/i }));
2434
- expect(writeText).toHaveBeenCalledWith('T-101');
2435
- expect(screen.getByRole('button', { name: /copied task id/i })).toBeInTheDocument();
2436
- });
2437
- it('copies the originating image reference from the annotator header', async () => {
2438
- const user = userEvent.setup();
2439
- const writeText = vi.fn().mockResolvedValue(undefined);
2440
- Object.defineProperty(navigator, 'clipboard', {
2441
- configurable: true,
2442
- value: { writeText },
2443
- });
2444
- mockAutoCreateFirstSession();
2445
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2446
- taskId: 'task-1',
2447
- taskReferenceLabel: 'T-101',
2448
- imageReferenceLabel: 'I-22',
2449
- assetId: 'asset-image-1',
2450
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2451
- displayName: 'Homepage shot',
2452
- } }));
2453
- await findSessionCard('Homepage shot review');
2454
- await user.click(screen.getByRole('button', { name: /copy image reference/i }));
2455
- expect(writeText).toHaveBeenCalledWith('I-22');
2456
- expect(screen.getByRole('button', { name: /copied image reference/i })).toBeInTheDocument();
2457
- });
2458
- it('auto-saves marker instruction edits after the debounce window', async () => {
2459
- fetchTaskforceApi
2460
- .mockResolvedValueOnce({
2461
- ok: true,
2462
- json: async () => ({
2463
- sessions: [
2464
- buildSessionResponse({
2465
- globalInstruction: 'Review this screenshot',
2466
- annotations: [
2467
- {
2468
- id: 'annotation-1',
2469
- order: 0,
2470
- kind: 'pin',
2471
- x: 0.2,
2472
- y: 0.3,
2473
- instruction: 'Inspect this button',
2474
- markerType: 'review',
2475
- color: '#0f766e',
2476
- },
2477
- ],
2478
- }),
2479
- ],
2480
- }),
2481
- })
2482
- .mockResolvedValueOnce({
2483
- ok: true,
2484
- json: async () => ({
2485
- session: buildSessionResponse({
2486
- globalInstruction: 'Review this screenshot',
2487
- updatedAt: '2026-03-25T12:05:00.000Z',
2488
- annotations: [
2489
- {
2490
- id: 'annotation-1',
2491
- order: 0,
2492
- kind: 'pin',
2493
- x: 0.2,
2494
- y: 0.3,
2495
- instruction: 'Inspect this header CTA',
2496
- markerType: 'review',
2497
- color: '#0f766e',
2498
- },
2499
- ],
2500
- }),
2501
- }),
2502
- });
2503
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2504
- taskId: 'task-1',
2505
- taskReferenceLabel: 'T-101',
2506
- assetId: 'asset-image-1',
2507
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2508
- displayName: 'Homepage shot',
2509
- } }));
2510
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2511
- const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2512
- fireEvent.change(instructionField, { target: { value: 'Inspect this header CTA' } });
2513
- expect(screen.getByText('Saving…')).toBeInTheDocument();
2514
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2515
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2516
- expect(saveBody.annotations[0]?.instruction).toBe('Inspect this header CTA');
2517
- await waitFor(() => expect(screen.getByText('Saved')).toBeInTheDocument());
2518
- });
2519
- it('flushes marker instruction edits immediately on blur', async () => {
2520
- fetchTaskforceApi
2521
- .mockResolvedValueOnce({
2522
- ok: true,
2523
- json: async () => ({
2524
- sessions: [
2525
- buildSessionResponse({
2526
- globalInstruction: 'Review this screenshot',
2527
- annotations: [
2528
- {
2529
- id: 'annotation-1',
2530
- order: 0,
2531
- kind: 'pin',
2532
- x: 0.2,
2533
- y: 0.3,
2534
- instruction: 'Inspect this button',
2535
- markerType: 'review',
2536
- color: '#0f766e',
2537
- },
2538
- ],
2539
- }),
2540
- ],
2541
- }),
2542
- })
2543
- .mockResolvedValueOnce({
2544
- ok: true,
2545
- json: async () => ({
2546
- session: buildSessionResponse({
2547
- globalInstruction: 'Review this screenshot',
2548
- updatedAt: '2026-03-25T12:05:00.000Z',
2549
- annotations: [
2550
- {
2551
- id: 'annotation-1',
2552
- order: 0,
2553
- kind: 'pin',
2554
- x: 0.2,
2555
- y: 0.3,
2556
- instruction: 'Inspect this header CTA',
2557
- markerType: 'review',
2558
- color: '#0f766e',
2559
- },
2560
- ],
2561
- }),
2562
- }),
2563
- });
2564
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2565
- taskId: 'task-1',
2566
- taskReferenceLabel: 'T-101',
2567
- assetId: 'asset-image-1',
2568
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2569
- displayName: 'Homepage shot',
2570
- } }));
2571
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2572
- const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2573
- fireEvent.change(instructionField, { target: { value: 'Inspect this header CTA' } });
2574
- fireEvent.blur(instructionField);
2575
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2576
- const saveBody = JSON.parse(String(fetchTaskforceApi.mock.calls[1]?.[1]?.body || '{}'));
2577
- expect(saveBody.annotations[0]?.instruction).toBe('Inspect this header CTA');
2578
- });
2579
- it('clears the previous session state immediately when switching to a new target', async () => {
2580
- const oldSession = buildSessionResponse({
2581
- id: 'session-old',
2582
- title: 'Old session',
2583
- annotations: [
2584
- {
2585
- id: 'annotation-1',
2586
- order: 0,
2587
- kind: 'pin',
2588
- x: 0.2,
2589
- y: 0.3,
2590
- instruction: 'Old note',
2591
- markerType: 'review',
2592
- color: '#0f766e',
2593
- },
2594
- ],
2595
- });
2596
- fetchTaskforceApi
2597
- .mockResolvedValueOnce({
2598
- ok: true,
2599
- json: async () => ({ sessions: [oldSession] }),
2600
- })
2601
- .mockResolvedValueOnce({
2602
- ok: true,
2603
- json: async () => ({ sessions: [] }),
2604
- })
2605
- .mockResolvedValueOnce({
2606
- ok: true,
2607
- json: async () => ({
2608
- session: buildSessionResponse({
2609
- id: 'session-new',
2610
- taskId: '',
2611
- baseImageAssetId: 'asset-image-2',
2612
- title: 'New session',
2613
- annotations: [],
2614
- }),
2615
- }),
2616
- });
2617
- function ControlledTargetHarness() {
2618
- const [target, setTarget] = React.useState({
2619
- taskId: 'task-1',
2620
- assetId: 'asset-image-1',
2621
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2622
- displayName: 'Old image',
2623
- });
2624
- return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => setTarget({
2625
- taskId: '',
2626
- assetId: 'asset-image-2',
2627
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-2.png',
2628
- displayName: 'New image',
2629
- }), children: "Switch target" }), _jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: target })] }));
2630
- }
2631
- const user = userEvent.setup();
2632
- render(_jsx(ControlledTargetHarness, {}));
2633
- expect(await findSessionCard('Old session')).toBeInTheDocument();
2634
- expect(screen.getByText('Old note')).toBeInTheDocument();
2635
- await user.click(screen.getByRole('button', { name: /switch target/i }));
2636
- await waitFor(() => {
2637
- expect(screen.queryByText('Old session')).not.toBeInTheDocument();
2638
- expect(screen.queryByText('Old note')).not.toBeInTheDocument();
2639
- expect(screen.getByText('New session')).toBeInTheDocument();
2640
- });
2641
- });
2642
- it('flushes pending edits before loading the payload preview', async () => {
2643
- const user = userEvent.setup();
2644
- fetchTaskforceApi
2645
- .mockResolvedValueOnce({
2646
- ok: true,
2647
- json: async () => ({
2648
- sessions: [
2649
- buildSessionResponse({
2650
- globalInstruction: 'Review this screenshot',
2651
- annotations: [
2652
- {
2653
- id: 'annotation-1',
2654
- order: 0,
2655
- kind: 'pin',
2656
- x: 0.2,
2657
- y: 0.3,
2658
- instruction: 'Inspect this button',
2659
- markerType: 'review',
2660
- color: '#0f766e',
2661
- },
2662
- ],
2663
- }),
2664
- ],
2665
- }),
2666
- })
2667
- .mockResolvedValueOnce({
2668
- ok: true,
2669
- json: async () => ({
2670
- session: buildSessionResponse({
2671
- globalInstruction: 'Review this screenshot',
2672
- updatedAt: '2026-03-25T12:05:00.000Z',
2673
- annotations: [
2674
- {
2675
- id: 'annotation-1',
2676
- order: 0,
2677
- kind: 'pin',
2678
- x: 0.2,
2679
- y: 0.3,
2680
- instruction: 'Inspect the signup button',
2681
- markerType: 'review',
2682
- color: '#0f766e',
2683
- },
2684
- ],
2685
- }),
2686
- }),
2687
- })
2688
- .mockResolvedValueOnce({
2689
- ok: true,
2690
- json: async () => ({
2691
- payload: {
2692
- taskId: 'task-1',
2693
- workspaceId: 'workspace-1',
2694
- sessionId: 'session-1',
2695
- title: 'Homepage shot review',
2696
- globalInstruction: 'Review this screenshot',
2697
- image: {
2698
- assetId: 'asset-image-1',
2699
- storageKey: 'workspaces/workspace-1/assets/images/asset-image-1.png',
2700
- apiUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2701
- mimeType: 'image/png',
2702
- originalFilename: 'homepage.png',
2703
- logicalName: 'homepage',
2704
- displayName: 'Homepage shot',
2705
- sizeBytes: 1024,
2706
- updatedAt: '2026-03-25T12:00:00.000Z',
2707
- },
2708
- annotations: [
2709
- {
2710
- id: 'annotation-1',
2711
- order: 0,
2712
- kind: 'pin',
2713
- x: 0.2,
2714
- y: 0.3,
2715
- instruction: 'Inspect the signup button',
2716
- markerType: 'review',
2717
- color: '#0f766e',
2718
- },
2719
- ],
2720
- },
2721
- }),
2722
- });
2723
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2724
- taskId: 'task-1',
2725
- taskReferenceLabel: 'T-101',
2726
- assetId: 'asset-image-1',
2727
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2728
- displayName: 'Homepage shot',
2729
- } }));
2730
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2731
- fireEvent.change(screen.getByRole('textbox', { name: /^marker instruction$/i }), {
2732
- target: { value: 'Inspect the signup button' },
2733
- });
2734
- await user.click(screen.getByRole('button', { name: /preview payload/i }));
2735
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2736
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1/payload?workspaceId=workspace-1', expect.objectContaining({ credentials: 'include' }), ''));
2737
- expect(await screen.findByText(/"Inspect the signup button"/)).toBeInTheDocument();
2738
- });
2739
- it('flushes the current draft before switching sessions', async () => {
2740
- const user = userEvent.setup();
2741
- fetchTaskforceApi
2742
- .mockResolvedValueOnce({
2743
- ok: true,
2744
- json: async () => ({
2745
- sessions: [
2746
- buildSessionResponse({
2747
- id: 'session-1',
2748
- title: 'Homepage review',
2749
- globalInstruction: 'Review the homepage',
2750
- annotations: [
2751
- {
2752
- id: 'annotation-1',
2753
- order: 0,
2754
- kind: 'pin',
2755
- x: 0.2,
2756
- y: 0.3,
2757
- instruction: 'Inspect this button',
2758
- markerType: 'review',
2759
- color: '#0f766e',
2760
- },
2761
- ],
2762
- }),
2763
- buildSessionResponse({
2764
- id: 'session-2',
2765
- title: 'Secondary review',
2766
- globalInstruction: 'Check spacing',
2767
- annotations: [],
2768
- updatedAt: '2026-03-25T12:08:00.000Z',
2769
- }),
2770
- ],
2771
- }),
2772
- })
2773
- .mockResolvedValueOnce({
2774
- ok: true,
2775
- json: async () => ({
2776
- session: buildSessionResponse({
2777
- id: 'session-1',
2778
- title: 'Homepage review',
2779
- globalInstruction: 'Review the homepage',
2780
- updatedAt: '2026-03-25T12:06:00.000Z',
2781
- annotations: [
2782
- {
2783
- id: 'annotation-1',
2784
- order: 0,
2785
- kind: 'pin',
2786
- x: 0.2,
2787
- y: 0.3,
2788
- instruction: 'Inspect the hero CTA',
2789
- markerType: 'review',
2790
- color: '#0f766e',
2791
- },
2792
- ],
2793
- }),
2794
- }),
2795
- });
2796
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2797
- taskId: 'task-1',
2798
- taskReferenceLabel: 'T-101',
2799
- assetId: 'asset-image-1',
2800
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2801
- displayName: 'Homepage shot',
2802
- } }));
2803
- expect(await findSessionCard('Homepage review')).toBeInTheDocument();
2804
- fireEvent.change(screen.getByRole('textbox', { name: /^marker instruction$/i }), {
2805
- target: { value: 'Inspect the hero CTA before switching sessions' },
2806
- });
2807
- await user.click(screen.getByRole('button', { name: /secondary review/i }));
2808
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2809
- expect(await screen.findByText('0 in this session')).toBeInTheDocument();
2810
- expect(screen.getByText('Add a marker on the image to begin.')).toBeInTheDocument();
2811
- });
2812
- it('keeps local edits visible after a save failure and allows manual retry', async () => {
2813
- const user = userEvent.setup();
2814
- fetchTaskforceApi
2815
- .mockResolvedValueOnce({
2816
- ok: true,
2817
- json: async () => ({
2818
- sessions: [
2819
- buildSessionResponse({
2820
- globalInstruction: 'Review this screenshot',
2821
- annotations: [
2822
- {
2823
- id: 'annotation-1',
2824
- order: 0,
2825
- kind: 'pin',
2826
- x: 0.2,
2827
- y: 0.3,
2828
- instruction: 'Inspect this button',
2829
- markerType: 'review',
2830
- color: '#0f766e',
2831
- },
2832
- ],
2833
- }),
2834
- ],
2835
- }),
2836
- })
2837
- .mockResolvedValueOnce({
2838
- ok: false,
2839
- json: async () => ({
2840
- error: 'Save failed on the server.',
2841
- }),
2842
- })
2843
- .mockResolvedValueOnce({
2844
- ok: true,
2845
- json: async () => ({
2846
- session: buildSessionResponse({
2847
- globalInstruction: 'Review this screenshot',
2848
- updatedAt: '2026-03-25T12:07:00.000Z',
2849
- annotations: [
2850
- {
2851
- id: 'annotation-1',
2852
- order: 0,
2853
- kind: 'pin',
2854
- x: 0.2,
2855
- y: 0.3,
2856
- instruction: 'Inspect the purchase button',
2857
- markerType: 'review',
2858
- color: '#0f766e',
2859
- },
2860
- ],
2861
- }),
2862
- }),
2863
- });
2864
- render(_jsx(AnnotatedAttachmentWorkspaceShell, { workspaceId: "workspace-1", requestedTarget: {
2865
- taskId: 'task-1',
2866
- taskReferenceLabel: 'T-101',
2867
- assetId: 'asset-image-1',
2868
- path: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fasset-image-1.png',
2869
- displayName: 'Homepage shot',
2870
- } }));
2871
- expect(await findSessionCard('Homepage shot review')).toBeInTheDocument();
2872
- const instructionField = screen.getByRole('textbox', { name: /^marker instruction$/i });
2873
- fireEvent.change(instructionField, { target: { value: 'Inspect the purchase button' } });
2874
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(2, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2875
- expect(screen.getByDisplayValue('Inspect the purchase button')).toBeInTheDocument();
2876
- expect(screen.getByText('Save failed. Retry now.')).toBeInTheDocument();
2877
- await user.click(screen.getByRole('button', { name: /save session/i }));
2878
- await waitFor(() => expect(fetchTaskforceApi).toHaveBeenNthCalledWith(3, '/api/taskforce/annotated-attachments/sessions/session-1?workspaceId=workspace-1', expect.objectContaining({ method: 'PATCH' }), ''));
2879
- const retryBody = JSON.parse(String(fetchTaskforceApi.mock.calls[2]?.[1]?.body || '{}'));
2880
- expect(retryBody.annotations[0]?.instruction).toBe('Inspect the purchase button');
2881
- await waitFor(() => expect(screen.queryByText('Save failed. Retry now.')).not.toBeInTheDocument());
2882
- });
2883
- });