@taskforcehq/taskforce 0.3.309 → 0.3.313

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (470) hide show
  1. package/README.md +89 -127
  2. package/dist/TaskforceCore.js +1 -1
  3. package/dist/components/context/ContextAttachmentManager.js +4 -3
  4. package/dist/components/features/InitiativesModule.js +1 -1
  5. package/dist/components/features/TaskSettings.js +1 -1
  6. package/dist/components/ui/DisclosureSectionHeader.d.ts +12 -0
  7. package/dist/components/ui/DisclosureSectionHeader.js +8 -0
  8. package/dist/components/views/PlanComparisonPage.js +1 -1
  9. package/dist/components/views/StandaloneLayout.js +18 -4
  10. package/dist/components/views/panels/PlanningDrawer.js +2 -1
  11. package/dist/components/views/standalone/modals/AccountHubModal.js +1 -1
  12. package/dist/components/views/standalone/modals/HelpModal.d.ts +3 -1
  13. package/dist/components/views/standalone/modals/HelpModal.js +17 -2
  14. package/dist/core/PlanEntitlementService.d.ts +2 -0
  15. package/dist/core/PlanEntitlementService.js +57 -4
  16. package/dist/hooks/useFilterSort.js +1 -1
  17. package/dist/hooks/useTaskforce.js +2 -2
  18. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +5 -2
  19. package/dist/localization/locales/en-US.js +1 -1
  20. package/dist/localization/locales/es-419.js +1 -1
  21. package/dist/localization/locales/pt-BR.js +1 -1
  22. package/dist/mcp/runtime.js +49 -14
  23. package/dist/server/routes/billing.js +0 -1
  24. package/dist/ui/assets/AgentsModule-BULqIo8e.js +1 -0
  25. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CCckzWYZ.js → AnnotatedAttachmentWorkspace-DHr4rHK9.js} +1 -1
  26. package/dist/ui/assets/ContextAttachmentManager-DoMe2OpY.js +3 -0
  27. package/dist/ui/assets/{DocumentWorkspace-BVaWPA25.js → DocumentWorkspace-DumBWhvb.js} +1 -1
  28. package/dist/ui/assets/{EntityActivityTimeline-DSj0ThaK.js → EntityActivityTimeline-sHk8Nzbq.js} +1 -1
  29. package/dist/ui/assets/{InitiativesModule-AxMkWtxH.js → InitiativesModule-3X9UNzHh.js} +1 -1
  30. package/dist/ui/assets/PlansPage-C3A3EYaV.js +1 -0
  31. package/dist/ui/assets/PlansPage-DeAzWYTb.css +1 -0
  32. package/dist/ui/assets/{TaskContextUpload-Dw4rTF4i.js → TaskContextUpload-C8G7pWkX.js} +1 -1
  33. package/dist/ui/assets/{TaskSettings-f2ga42_V.js → TaskSettings-DM1o9Wla.js} +2 -2
  34. package/dist/ui/assets/{WorkflowsModule-E4UL3mov.js → WorkflowsModule-CW0crzvD.js} +1 -1
  35. package/dist/ui/assets/documentReferences-BZB3nMRb.js +1 -0
  36. package/dist/ui/assets/index-BWFQ9mGn.js +5 -0
  37. package/dist/ui/assets/{index-BvAbqx5Q.css → index-DcK_8tuE.css} +1 -1
  38. package/dist/ui/assets/{vendor-icons-B32hGuF2.js → vendor-icons-CLnehDTw.js} +1 -1
  39. package/dist/ui/index.html +3 -3
  40. package/dist/utils/supportContact.d.ts +9 -0
  41. package/dist/utils/supportContact.js +32 -0
  42. package/package.json +12 -4
  43. package/scripts/postinstall.js +4 -3
  44. package/dist/TaskforceCore.CategoryFallback.test.d.ts +0 -1
  45. package/dist/TaskforceCore.CategoryFallback.test.js +0 -101
  46. package/dist/TaskforceCore.filter-persistence.test.d.ts +0 -1
  47. package/dist/TaskforceCore.filter-persistence.test.js +0 -402
  48. package/dist/TaskforceCore.routing.test.d.ts +0 -1
  49. package/dist/TaskforceCore.routing.test.js +0 -311
  50. package/dist/TaskforceCore.test.d.ts +0 -1
  51. package/dist/TaskforceCore.test.js +0 -7910
  52. package/dist/TaskforceWidget.test.d.ts +0 -1
  53. package/dist/TaskforceWidget.test.js +0 -92
  54. package/dist/UnsavedChangesRelaxed.test.d.ts +0 -1
  55. package/dist/UnsavedChangesRelaxed.test.js +0 -16
  56. package/dist/__tests__/localizationCatalogs.test.d.ts +0 -1
  57. package/dist/__tests__/localizationCatalogs.test.js +0 -54
  58. package/dist/__tests__/workflowSimulation.test.d.ts +0 -1
  59. package/dist/__tests__/workflowSimulation.test.js +0 -113
  60. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts +0 -1
  61. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +0 -278
  62. package/dist/annotatedAttachments/payload.test.d.ts +0 -1
  63. package/dist/annotatedAttachments/payload.test.js +0 -67
  64. package/dist/compat/workspaceSyncCompat.test.d.ts +0 -1
  65. package/dist/compat/workspaceSyncCompat.test.js +0 -124
  66. package/dist/components/features/AgentsModule.test.d.ts +0 -1
  67. package/dist/components/features/AgentsModule.test.js +0 -601
  68. package/dist/components/features/AnnotatedAttachmentWorkspace.test.d.ts +0 -1
  69. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +0 -2883
  70. package/dist/components/features/DocumentWorkspace.test.d.ts +0 -1
  71. package/dist/components/features/DocumentWorkspace.test.js +0 -1439
  72. package/dist/components/features/TaskSettings.test.d.ts +0 -1
  73. package/dist/components/features/TaskSettings.test.js +0 -1674
  74. package/dist/components/features/settings/TaxonomyLibraryManager.test.d.ts +0 -1
  75. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +0 -141
  76. package/dist/components/task/TaskActionHeader.test.d.ts +0 -1
  77. package/dist/components/task/TaskActionHeader.test.js +0 -91
  78. package/dist/components/task/TaskCard.test.d.ts +0 -1
  79. package/dist/components/task/TaskCard.test.js +0 -513
  80. package/dist/components/task/TaskContextUpload.test.d.ts +0 -1
  81. package/dist/components/task/TaskContextUpload.test.js +0 -506
  82. package/dist/components/task/TaskForm.test.d.ts +0 -1
  83. package/dist/components/task/TaskForm.test.js +0 -946
  84. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +0 -1
  85. package/dist/components/task/TaskKanban.empty-column-drop.test.js +0 -66
  86. package/dist/components/task/TaskKanban.planning-drop.test.d.ts +0 -1
  87. package/dist/components/task/TaskKanban.planning-drop.test.js +0 -93
  88. package/dist/components/task/TaskKanban.test.d.ts +0 -1
  89. package/dist/components/task/TaskKanban.test.js +0 -99
  90. package/dist/components/task/TaskList.test.d.ts +0 -1
  91. package/dist/components/task/TaskList.test.js +0 -473
  92. package/dist/components/task/TaskStatusActions.test.d.ts +0 -1
  93. package/dist/components/task/TaskStatusActions.test.js +0 -54
  94. package/dist/components/task/taskKanbanDropRules.test.d.ts +0 -1
  95. package/dist/components/task/taskKanbanDropRules.test.js +0 -114
  96. package/dist/components/ui/LevelSelector.test.d.ts +0 -1
  97. package/dist/components/ui/LevelSelector.test.js +0 -55
  98. package/dist/components/ui/Markdown.test.d.ts +0 -1
  99. package/dist/components/ui/Markdown.test.js +0 -38
  100. package/dist/components/ui/MultiSelectFilter.test.d.ts +0 -1
  101. package/dist/components/ui/MultiSelectFilter.test.js +0 -74
  102. package/dist/components/ui/TaxonomyDropdown.test.d.ts +0 -1
  103. package/dist/components/ui/TaxonomyDropdown.test.js +0 -130
  104. package/dist/components/ui/TopNoticeLayer.test.d.ts +0 -1
  105. package/dist/components/ui/TopNoticeLayer.test.js +0 -25
  106. package/dist/components/views/AppShellHeader.test.d.ts +0 -1
  107. package/dist/components/views/AppShellHeader.test.js +0 -42
  108. package/dist/components/views/PlanComparisonPage.test.d.ts +0 -1
  109. package/dist/components/views/PlanComparisonPage.test.js +0 -757
  110. package/dist/components/views/PlansPage.test.d.ts +0 -1
  111. package/dist/components/views/PlansPage.test.js +0 -710
  112. package/dist/components/views/chrome/WorkspaceChromeSections.test.d.ts +0 -1
  113. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +0 -54
  114. package/dist/components/views/chrome/workspaceChrome.test.d.ts +0 -1
  115. package/dist/components/views/chrome/workspaceChrome.test.js +0 -52
  116. package/dist/components/views/panels/DocumentFilterToolbar.test.d.ts +0 -1
  117. package/dist/components/views/panels/DocumentFilterToolbar.test.js +0 -19
  118. package/dist/components/views/panels/FilterToolbar.test.d.ts +0 -1
  119. package/dist/components/views/panels/FilterToolbar.test.js +0 -47
  120. package/dist/components/views/panels/PlanningDrawer.test.d.ts +0 -1
  121. package/dist/components/views/panels/PlanningDrawer.test.js +0 -529
  122. package/dist/components/views/planningScope.test.d.ts +0 -1
  123. package/dist/components/views/planningScope.test.js +0 -60
  124. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +0 -1
  125. package/dist/components/views/standalone/modals/AccountHubModal.test.js +0 -66
  126. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +0 -1
  127. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +0 -24
  128. package/dist/components/views/teamManagementAccess.test.d.ts +0 -1
  129. package/dist/components/views/teamManagementAccess.test.js +0 -44
  130. package/dist/config/deployment.test.d.ts +0 -1
  131. package/dist/config/deployment.test.js +0 -90
  132. package/dist/config/envSchema.test.d.ts +0 -1
  133. package/dist/config/envSchema.test.js +0 -125
  134. package/dist/core/AiProfileService.test.d.ts +0 -1
  135. package/dist/core/AiProfileService.test.js +0 -184
  136. package/dist/core/AiProfiles.test.d.ts +0 -1
  137. package/dist/core/AiProfiles.test.js +0 -1873
  138. package/dist/core/AuthTokenService.test.d.ts +0 -1
  139. package/dist/core/AuthTokenService.test.js +0 -78
  140. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +0 -1
  141. package/dist/core/BulkUpdateFieldsPreview.test.js +0 -62
  142. package/dist/core/CompletedAt.test.d.ts +0 -1
  143. package/dist/core/CompletedAt.test.js +0 -44
  144. package/dist/core/CreatedBy.test.d.ts +0 -1
  145. package/dist/core/CreatedBy.test.js +0 -188
  146. package/dist/core/DeletedTaskTrash.test.d.ts +0 -1
  147. package/dist/core/DeletedTaskTrash.test.js +0 -104
  148. package/dist/core/EntitlementsPolicy.test.d.ts +0 -1
  149. package/dist/core/EntitlementsPolicy.test.js +0 -280
  150. package/dist/core/GlobalSettingsService.test.d.ts +0 -1
  151. package/dist/core/GlobalSettingsService.test.js +0 -45
  152. package/dist/core/JsonBackupRuntime.test.d.ts +0 -1
  153. package/dist/core/JsonBackupRuntime.test.js +0 -155
  154. package/dist/core/PlanFeatureCatalog.test.d.ts +0 -1
  155. package/dist/core/PlanFeatureCatalog.test.js +0 -139
  156. package/dist/core/PlanVersionPolicy.test.d.ts +0 -1
  157. package/dist/core/PlanVersionPolicy.test.js +0 -204
  158. package/dist/core/PlanningEntities.test.d.ts +0 -1
  159. package/dist/core/PlanningEntities.test.js +0 -509
  160. package/dist/core/ScheduleFields.test.d.ts +0 -1
  161. package/dist/core/ScheduleFields.test.js +0 -193
  162. package/dist/core/SetupState.test.d.ts +0 -1
  163. package/dist/core/SetupState.test.js +0 -113
  164. package/dist/core/SignupMonetizationSettings.test.d.ts +0 -1
  165. package/dist/core/SignupMonetizationSettings.test.js +0 -515
  166. package/dist/core/Stats.test.d.ts +0 -1
  167. package/dist/core/Stats.test.js +0 -36
  168. package/dist/core/SystemAdmin.test.d.ts +0 -1
  169. package/dist/core/SystemAdmin.test.js +0 -1106
  170. package/dist/core/TaskActivitySync.test.d.ts +0 -1
  171. package/dist/core/TaskActivitySync.test.js +0 -90
  172. package/dist/core/TaskAttachmentsCanonical.test.d.ts +0 -1
  173. package/dist/core/TaskAttachmentsCanonical.test.js +0 -356
  174. package/dist/core/TaskAuditTimeline.test.d.ts +0 -1
  175. package/dist/core/TaskAuditTimeline.test.js +0 -319
  176. package/dist/core/TaskTaxonomyValidation.test.d.ts +0 -1
  177. package/dist/core/TaskTaxonomyValidation.test.js +0 -440
  178. package/dist/core/Taskforce.ids.test.d.ts +0 -1
  179. package/dist/core/Taskforce.ids.test.js +0 -611
  180. package/dist/core/Taskforce.listTasksSlim.test.d.ts +0 -1
  181. package/dist/core/Taskforce.listTasksSlim.test.js +0 -91
  182. package/dist/core/Taskforce.mcpAuth.test.d.ts +0 -1
  183. package/dist/core/Taskforce.mcpAuth.test.js +0 -450
  184. package/dist/core/Taskforce.realtimeMutation.test.d.ts +0 -1
  185. package/dist/core/Taskforce.realtimeMutation.test.js +0 -131
  186. package/dist/core/UserProfileAvatarDrafts.test.d.ts +0 -1
  187. package/dist/core/UserProfileAvatarDrafts.test.js +0 -138
  188. package/dist/core/WorkspaceDelete.test.d.ts +0 -1
  189. package/dist/core/WorkspaceDelete.test.js +0 -159
  190. package/dist/core/WorkspacePermissions.test.d.ts +0 -1
  191. package/dist/core/WorkspacePermissions.test.js +0 -270
  192. package/dist/core/WorkspacePlanMode.test.d.ts +0 -1
  193. package/dist/core/WorkspacePlanMode.test.js +0 -383
  194. package/dist/core/WorkspaceSyncSettings.test.d.ts +0 -1
  195. package/dist/core/WorkspaceSyncSettings.test.js +0 -65
  196. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +0 -1
  197. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +0 -212
  198. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +0 -1
  199. package/dist/documentReviews/DocumentReviewSessionStore.test.js +0 -127
  200. package/dist/hooks/authGuardPolicy.test.d.ts +0 -1
  201. package/dist/hooks/authGuardPolicy.test.js +0 -76
  202. package/dist/hooks/bootstrapTimeouts.test.d.ts +0 -1
  203. package/dist/hooks/bootstrapTimeouts.test.js +0 -12
  204. package/dist/hooks/sync/controlPlane.test.d.ts +0 -1
  205. package/dist/hooks/sync/controlPlane.test.js +0 -121
  206. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +0 -1
  207. package/dist/hooks/sync/lifecyclePolicy.test.js +0 -124
  208. package/dist/hooks/sync/orchestratorShared.test.d.ts +0 -1
  209. package/dist/hooks/sync/orchestratorShared.test.js +0 -98
  210. package/dist/hooks/sync/pullLifecycle.test.d.ts +0 -1
  211. package/dist/hooks/sync/pullLifecycle.test.js +0 -80
  212. package/dist/hooks/sync/recovery.test.d.ts +0 -1
  213. package/dist/hooks/sync/recovery.test.js +0 -292
  214. package/dist/hooks/sync/transfers.test.d.ts +0 -1
  215. package/dist/hooks/sync/transfers.test.js +0 -928
  216. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +0 -1
  217. package/dist/hooks/sync/useRecentSyncEvents.test.js +0 -124
  218. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +0 -1
  219. package/dist/hooks/sync/useSyncStatusActions.test.js +0 -133
  220. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +0 -1
  221. package/dist/hooks/sync/useSyncStatusControls.test.js +0 -88
  222. package/dist/hooks/useFilterSort.test.d.ts +0 -1
  223. package/dist/hooks/useFilterSort.test.js +0 -419
  224. package/dist/hooks/useRealtimeSync.test.d.ts +0 -1
  225. package/dist/hooks/useRealtimeSync.test.js +0 -311
  226. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +0 -1
  227. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +0 -54
  228. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +0 -1
  229. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +0 -228
  230. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +0 -1
  231. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +0 -2027
  232. package/dist/hooks/useTaskData.test.d.ts +0 -1
  233. package/dist/hooks/useTaskData.test.js +0 -291
  234. package/dist/hooks/useTaskMutations.test.d.ts +0 -1
  235. package/dist/hooks/useTaskMutations.test.js +0 -256
  236. package/dist/hooks/useTaskforce.contract.test.d.ts +0 -1
  237. package/dist/hooks/useTaskforce.contract.test.js +0 -7
  238. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +0 -1
  239. package/dist/hooks/useTaskforce.priority-filters.test.js +0 -28
  240. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +0 -1
  241. package/dist/hooks/useTaskforce.runtime-routing.test.js +0 -246
  242. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +0 -1
  243. package/dist/hooks/useTaskforce.sync-behavior.test.js +0 -3644
  244. package/dist/hooks/useWorkspaceSyncController.test.d.ts +0 -1
  245. package/dist/hooks/useWorkspaceSyncController.test.js +0 -192
  246. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +0 -1
  247. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +0 -77
  248. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +0 -1
  249. package/dist/hooks/workspace/workspaceLocalState.test.js +0 -59
  250. package/dist/localization/localizationCatalogs.test.d.ts +0 -1
  251. package/dist/localization/localizationCatalogs.test.js +0 -37
  252. package/dist/mcp/aiProfileBootstrap.test.d.ts +0 -1
  253. package/dist/mcp/aiProfileBootstrap.test.js +0 -68
  254. package/dist/mcp/canonicalAssetHelpers.test.d.ts +0 -1
  255. package/dist/mcp/canonicalAssetHelpers.test.js +0 -67
  256. package/dist/mcp/clientIntegrations.test.d.ts +0 -1
  257. package/dist/mcp/clientIntegrations.test.js +0 -219
  258. package/dist/mcp/optionResolution.test.d.ts +0 -1
  259. package/dist/mcp/optionResolution.test.js +0 -43
  260. package/dist/mcp/runtime.test.d.ts +0 -1
  261. package/dist/mcp/runtime.test.js +0 -5120
  262. package/dist/mcp/structuredCommentValidation.test.d.ts +0 -1
  263. package/dist/mcp/structuredCommentValidation.test.js +0 -31
  264. package/dist/mcp/structuredDocValidation.test.d.ts +0 -1
  265. package/dist/mcp/structuredDocValidation.test.js +0 -35
  266. package/dist/mcp/toolRegistry.test.d.ts +0 -1
  267. package/dist/mcp/toolRegistry.test.js +0 -34
  268. package/dist/migrations/billingSchemaParity.test.d.ts +0 -1
  269. package/dist/migrations/billingSchemaParity.test.js +0 -160
  270. package/dist/migrations/taskSchemaMigrations.test.d.ts +0 -1
  271. package/dist/migrations/taskSchemaMigrations.test.js +0 -225
  272. package/dist/qaOpen.test.d.ts +0 -1
  273. package/dist/qaOpen.test.js +0 -56
  274. package/dist/releaseVersion.test.d.ts +0 -1
  275. package/dist/releaseVersion.test.js +0 -32
  276. package/dist/runtime/appGates.test.d.ts +0 -1
  277. package/dist/runtime/appGates.test.js +0 -42
  278. package/dist/server/aiProfileAvatarAssets.test.d.ts +0 -1
  279. package/dist/server/aiProfileAvatarAssets.test.js +0 -213
  280. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +0 -1
  281. package/dist/server/annotatedAttachmentsRoutes.test.js +0 -532
  282. package/dist/server/auth.test.d.ts +0 -1
  283. package/dist/server/auth.test.js +0 -131
  284. package/dist/server/buildInfo.test.d.ts +0 -1
  285. package/dist/server/buildInfo.test.js +0 -29
  286. package/dist/server/cors.test.d.ts +0 -1
  287. package/dist/server/cors.test.js +0 -77
  288. package/dist/server/documentReviewRoutes.test.d.ts +0 -1
  289. package/dist/server/documentReviewRoutes.test.js +0 -190
  290. package/dist/server/email.test.d.ts +0 -1
  291. package/dist/server/email.test.js +0 -113
  292. package/dist/server/index.cookieProxy.test.d.ts +0 -1
  293. package/dist/server/index.cookieProxy.test.js +0 -67
  294. package/dist/server/index.rateLimit.test.d.ts +0 -1
  295. package/dist/server/index.rateLimit.test.js +0 -22
  296. package/dist/server/index.test.d.ts +0 -1
  297. package/dist/server/index.test.js +0 -283
  298. package/dist/server/rateLimit.test.d.ts +0 -1
  299. package/dist/server/rateLimit.test.js +0 -107
  300. package/dist/server/realtimeSync.auth.test.d.ts +0 -1
  301. package/dist/server/realtimeSync.auth.test.js +0 -62
  302. package/dist/server/realtimeSync.replay.test.d.ts +0 -1
  303. package/dist/server/realtimeSync.replay.test.js +0 -192
  304. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +0 -1
  305. package/dist/server/realtimeSync.workspaceIsolation.test.js +0 -101
  306. package/dist/server/routes/billing.test.d.ts +0 -1
  307. package/dist/server/routes/billing.test.js +0 -3455
  308. package/dist/server/routes/shared.test.d.ts +0 -1
  309. package/dist/server/routes/shared.test.js +0 -32
  310. package/dist/server/routes/sync.integration.test.d.ts +0 -1
  311. package/dist/server/routes/sync.integration.test.js +0 -2932
  312. package/dist/server/routes/syncAuxRoutes.test.d.ts +0 -1
  313. package/dist/server/routes/syncAuxRoutes.test.js +0 -158
  314. package/dist/server/routes/syncRouteGuards.test.d.ts +0 -1
  315. package/dist/server/routes/syncRouteGuards.test.js +0 -94
  316. package/dist/server/routes.test.d.ts +0 -1
  317. package/dist/server/routes.test.js +0 -15612
  318. package/dist/server/runtimeMode.test.d.ts +0 -1
  319. package/dist/server/runtimeMode.test.js +0 -26
  320. package/dist/server/securityHeaders.test.d.ts +0 -1
  321. package/dist/server/securityHeaders.test.js +0 -70
  322. package/dist/services/templateEngine.test.d.ts +0 -1
  323. package/dist/services/templateEngine.test.js +0 -150
  324. package/dist/services/templates.test.d.ts +0 -1
  325. package/dist/services/templates.test.js +0 -45
  326. package/dist/services/workflowDocumentFixtures.test.d.ts +0 -1
  327. package/dist/services/workflowDocumentFixtures.test.js +0 -78
  328. package/dist/services/workflowExportMatrix.test.d.ts +0 -1
  329. package/dist/services/workflowExportMatrix.test.js +0 -113
  330. package/dist/services/workflowExportSnapshots.test.d.ts +0 -1
  331. package/dist/services/workflowExportSnapshots.test.js +0 -30
  332. package/dist/services/workflowGeneration.test.d.ts +0 -1
  333. package/dist/services/workflowGeneration.test.js +0 -12
  334. package/dist/services/workflowSimulation.test.d.ts +0 -1
  335. package/dist/services/workflowSimulation.test.js +0 -113
  336. package/dist/services/workflowV2Policy.test.d.ts +0 -1
  337. package/dist/services/workflowV2Policy.test.js +0 -80
  338. package/dist/shared/customTaxonomyLibrary.test.d.ts +0 -1
  339. package/dist/shared/customTaxonomyLibrary.test.js +0 -20
  340. package/dist/shared/passwordPolicy.test.d.ts +0 -1
  341. package/dist/shared/passwordPolicy.test.js +0 -34
  342. package/dist/shared/runtimeContract.test.d.ts +0 -1
  343. package/dist/shared/runtimeContract.test.js +0 -23
  344. package/dist/shared/taxonomyLibrary.test.d.ts +0 -1
  345. package/dist/shared/taxonomyLibrary.test.js +0 -26
  346. package/dist/shared/taxonomyState.test.d.ts +0 -1
  347. package/dist/shared/taxonomyState.test.js +0 -71
  348. package/dist/soak/preflight.cli.d.ts +0 -1
  349. package/dist/soak/preflight.cli.js +0 -28
  350. package/dist/soak/preflight.d.ts +0 -26
  351. package/dist/soak/preflight.js +0 -113
  352. package/dist/soak/preflight.test.d.ts +0 -1
  353. package/dist/soak/preflight.test.js +0 -39
  354. package/dist/storage/objectStorageClient.test.d.ts +0 -1
  355. package/dist/storage/objectStorageClient.test.js +0 -82
  356. package/dist/storage/objectStorageConfig.test.d.ts +0 -1
  357. package/dist/storage/objectStorageConfig.test.js +0 -50
  358. package/dist/storage/postgresAdapter.test.d.ts +0 -1
  359. package/dist/storage/postgresAdapter.test.js +0 -29
  360. package/dist/storage/workspaceAssetStore.test.d.ts +0 -1
  361. package/dist/storage/workspaceAssetStore.test.js +0 -588
  362. package/dist/sync/cloudSyncApi.test.d.ts +0 -1
  363. package/dist/sync/cloudSyncApi.test.js +0 -111
  364. package/dist/sync/collaborationSyncPayload.test.d.ts +0 -1
  365. package/dist/sync/collaborationSyncPayload.test.js +0 -125
  366. package/dist/sync/contentSyncPayload.test.d.ts +0 -1
  367. package/dist/sync/contentSyncPayload.test.js +0 -143
  368. package/dist/sync/encryptionService.test.d.ts +0 -1
  369. package/dist/sync/encryptionService.test.js +0 -52
  370. package/dist/sync/planningSyncPayload.test.d.ts +0 -1
  371. package/dist/sync/planningSyncPayload.test.js +0 -92
  372. package/dist/sync/realtimeFeature.test.d.ts +0 -1
  373. package/dist/sync/realtimeFeature.test.js +0 -30
  374. package/dist/sync/realtimeReconcile.test.d.ts +0 -1
  375. package/dist/sync/realtimeReconcile.test.js +0 -43
  376. package/dist/sync/syncApplyHandlers.test.d.ts +0 -1
  377. package/dist/sync/syncApplyHandlers.test.js +0 -626
  378. package/dist/sync/syncService.test.d.ts +0 -1
  379. package/dist/sync/syncService.test.js +0 -903
  380. package/dist/sync/taskSyncPayload.test.d.ts +0 -1
  381. package/dist/sync/taskSyncPayload.test.js +0 -54
  382. package/dist/sync/workspacePullFeed.test.d.ts +0 -1
  383. package/dist/sync/workspacePullFeed.test.js +0 -438
  384. package/dist/sync/workspaceSyncModel.test.d.ts +0 -1
  385. package/dist/sync/workspaceSyncModel.test.js +0 -1442
  386. package/dist/test/setup.d.ts +0 -1
  387. package/dist/test/setup.js +0 -15
  388. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +0 -1
  389. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +0 -3
  390. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +0 -1
  391. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +0 -1
  392. package/dist/ui/assets/documentReferences-BOUcJm6-.js +0 -1
  393. package/dist/ui/assets/index-MAHKvFqp.js +0 -4
  394. package/dist/utils/accountProfileSummaryCache.test.d.ts +0 -1
  395. package/dist/utils/accountProfileSummaryCache.test.js +0 -63
  396. package/dist/utils/annotatedAttachments.test.d.ts +0 -1
  397. package/dist/utils/annotatedAttachments.test.js +0 -108
  398. package/dist/utils/assignees.test.d.ts +0 -1
  399. package/dist/utils/assignees.test.js +0 -25
  400. package/dist/utils/avatarUpload.test.d.ts +0 -1
  401. package/dist/utils/avatarUpload.test.js +0 -40
  402. package/dist/utils/canonicalAssetKind.test.d.ts +0 -1
  403. package/dist/utils/canonicalAssetKind.test.js +0 -24
  404. package/dist/utils/coalescedRefresh.test.d.ts +0 -1
  405. package/dist/utils/coalescedRefresh.test.js +0 -30
  406. package/dist/utils/constants.icons.test.d.ts +0 -1
  407. package/dist/utils/constants.icons.test.js +0 -41
  408. package/dist/utils/planParser.test.d.ts +0 -1
  409. package/dist/utils/planParser.test.js +0 -183
  410. package/dist/utils/planningReferences.test.d.ts +0 -1
  411. package/dist/utils/planningReferences.test.js +0 -17
  412. package/dist/utils/syncEventDetails.test.d.ts +0 -1
  413. package/dist/utils/syncEventDetails.test.js +0 -23
  414. package/dist/utils/syncEventPresentation.test.d.ts +0 -1
  415. package/dist/utils/syncEventPresentation.test.js +0 -64
  416. package/dist/utils/syncStatusPresentation.test.d.ts +0 -1
  417. package/dist/utils/syncStatusPresentation.test.js +0 -164
  418. package/dist/utils/taskActivity.test.d.ts +0 -1
  419. package/dist/utils/taskActivity.test.js +0 -218
  420. package/dist/utils/taskDraftDefaults.test.d.ts +0 -1
  421. package/dist/utils/taskDraftDefaults.test.js +0 -34
  422. package/dist/utils/taskDragZones.test.d.ts +0 -1
  423. package/dist/utils/taskDragZones.test.js +0 -14
  424. package/dist/utils/taskNormalization.test.d.ts +0 -1
  425. package/dist/utils/taskNormalization.test.js +0 -158
  426. package/dist/utils/taskSearch.test.d.ts +0 -1
  427. package/dist/utils/taskSearch.test.js +0 -69
  428. package/dist/utils/taskUtils.test.d.ts +0 -1
  429. package/dist/utils/taskUtils.test.js +0 -19
  430. package/dist/utils/taskforceApiClient.test.d.ts +0 -1
  431. package/dist/utils/taskforceApiClient.test.js +0 -44
  432. package/dist/utils/taxonomyFormDefaults.test.d.ts +0 -1
  433. package/dist/utils/taxonomyFormDefaults.test.js +0 -63
  434. package/dist/utils/uiNotice.test.d.ts +0 -1
  435. package/dist/utils/uiNotice.test.js +0 -18
  436. package/dist/utils/workspaceIdentity.test.d.ts +0 -1
  437. package/dist/utils/workspaceIdentity.test.js +0 -61
  438. package/dist/utils/workspaceLifecycle.test.d.ts +0 -1
  439. package/dist/utils/workspaceLifecycle.test.js +0 -64
  440. package/dist/utils/workspaceSyncPresentation.test.d.ts +0 -1
  441. package/dist/utils/workspaceSyncPresentation.test.js +0 -71
  442. package/scripts/bootstrap-playwright-auth.mjs +0 -155
  443. package/scripts/check-cloud-mcp.mjs +0 -83
  444. package/scripts/check-compat-boundaries.mjs +0 -41
  445. package/scripts/check-env.ts +0 -31
  446. package/scripts/check-localization-strings.mjs +0 -49
  447. package/scripts/check-runtime-boundaries.mjs +0 -44
  448. package/scripts/debug-global.mjs +0 -34
  449. package/scripts/debug-login.mjs +0 -32
  450. package/scripts/debug-server.mjs +0 -36
  451. package/scripts/deploy-prod.sh +0 -41
  452. package/scripts/deploy-staging.sh +0 -23
  453. package/scripts/export-sqlite-to-postgres.mjs +0 -123
  454. package/scripts/generate-env-example.ts +0 -8
  455. package/scripts/mcp-stdio-adapter.mjs +0 -160
  456. package/scripts/migrate-task-data-assets.mjs +0 -408
  457. package/scripts/playwright-auth.sh +0 -19
  458. package/scripts/qa-open-lib.mjs +0 -175
  459. package/scripts/qa-open-lib.mjs.d.ts +0 -49
  460. package/scripts/qa-open.mjs +0 -352
  461. package/scripts/release-version-lib.mjs +0 -88
  462. package/scripts/release-version.mjs +0 -72
  463. package/scripts/run-billing-performance-smoke.sh +0 -24
  464. package/scripts/run-dev-proxy.sh +0 -51
  465. package/scripts/run-e2e-realtime.sh +0 -30
  466. package/scripts/run-smoke.sh +0 -35
  467. package/scripts/run-soak.sh +0 -41
  468. package/scripts/test-global.js +0 -143
  469. package/scripts/update-homebrew-formula.sh +0 -108
  470. package/scripts/with-esbuild.sh +0 -11
@@ -1,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
- });