@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -1,26 +1,26 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import userEvent from '@testing-library/user-event';
4
- import { describe, it, expect, vi } from 'vitest';
5
- import { TaskForm } from './TaskForm';
6
- describe('TaskForm Component', () => {
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, it, expect, vi } from "vitest";
5
+ import { TaskForm } from "./TaskForm";
6
+ describe("TaskForm Component", () => {
7
7
  const defaultProps = {
8
8
  editingTaskId: null,
9
- error: '',
10
- title: '',
11
- description: '',
12
- category: 'general',
13
- type: 'feature',
9
+ error: "",
10
+ title: "",
11
+ description: "",
12
+ category: "general",
13
+ type: "feature",
14
14
  priority: 2,
15
15
  complexity: 3,
16
- approach: '',
17
- assignee: 'unassigned',
18
- scheduledDate: '',
19
- dueDate: '',
20
- workstreamInput: '',
16
+ approach: "",
17
+ assignee: "unassigned",
18
+ scheduledDate: "",
19
+ dueDate: "",
20
+ workstreamInput: "",
21
21
  checklistItems: [],
22
22
  comments: [],
23
- newCommentText: '',
23
+ newCommentText: "",
24
24
  contextFiles: [],
25
25
  descriptionFocused: false,
26
26
  showMarkdownHelp: false,
@@ -32,31 +32,59 @@ describe('TaskForm Component', () => {
32
32
  taxonomies: [],
33
33
  approaches: [],
34
34
  assigneeOptions: [
35
- { value: 'unassigned', label: 'Unassigned', icon: 'HelpCircle', color: 'var(--text-secondary)', kind: 'unassigned' },
36
- { value: 'member-1', label: 'Alex Example', icon: 'User', color: '#22c55e', kind: 'member' },
35
+ {
36
+ value: "unassigned",
37
+ label: "Unassigned",
38
+ icon: "HelpCircle",
39
+ color: "var(--text-secondary)",
40
+ kind: "unassigned",
41
+ },
42
+ {
43
+ value: "member-1",
44
+ label: "Alex Example",
45
+ icon: "User",
46
+ color: "#22c55e",
47
+ kind: "member",
48
+ },
37
49
  ],
38
50
  categories: [
39
- { value: 'general', label: 'General', disabled: false },
40
- { value: 'backend', label: 'Backend', disabled: false }
51
+ { value: "general", label: "General", disabled: false },
52
+ { value: "backend", label: "Backend", disabled: false },
53
+ ],
54
+ types: [
55
+ { value: "feature", label: "Feature" },
56
+ { value: "bug", label: "Bug" },
57
+ ],
58
+ priorities: [
59
+ { value: 3, label: "High" },
60
+ { value: 2, label: "Medium" },
41
61
  ],
42
- types: [{ value: 'feature', label: 'Feature' }, { value: 'bug', label: 'Bug' }],
43
- priorities: [{ value: 3, label: 'High' }, { value: 2, label: 'Medium' }],
44
62
  workstreams: [
45
63
  {
46
- id: 'workstream-1',
64
+ id: "workstream-1",
47
65
  referenceNumber: 12,
48
- referenceLabel: 'WS-12',
49
- title: 'Platform Refinement',
66
+ referenceLabel: "WS-12",
67
+ title: "Platform Refinement",
50
68
  description: null,
51
- createdAt: '2026-04-03T00:00:00.000Z',
69
+ createdAt: "2026-04-03T00:00:00.000Z",
52
70
  },
53
71
  {
54
- id: 'workstream-2',
72
+ id: "workstream-2",
55
73
  referenceNumber: 44,
56
- referenceLabel: 'WS-44',
57
- title: 'Growth Experiments',
74
+ referenceLabel: "WS-44",
75
+ title: "Growth Experiments",
58
76
  description: null,
59
- createdAt: '2026-04-03T00:00:00.000Z',
77
+ createdAt: "2026-04-03T00:00:00.000Z",
78
+ },
79
+ ],
80
+ initiatives: [
81
+ {
82
+ id: "initiative-1",
83
+ referenceNumber: 3,
84
+ referenceLabel: "IN-3",
85
+ title: "Execution model",
86
+ description: null,
87
+ createdAt: "2026-04-03T00:00:00.000Z",
60
88
  },
61
89
  ],
62
90
  copiedId: null,
@@ -80,7 +108,7 @@ describe('TaskForm Component', () => {
80
108
  onShowCategoryManagerChange: vi.fn(),
81
109
  onShowTypeManagerChange: vi.fn(),
82
110
  onConfiguringCategoryChange: vi.fn(),
83
- onSubmit: vi.fn(e => e.preventDefault()),
111
+ onSubmit: vi.fn((e) => e.preventDefault()),
84
112
  onAddComment: vi.fn(),
85
113
  onAddContextFile: vi.fn(),
86
114
  onRemoveContextFile: vi.fn(),
@@ -100,295 +128,325 @@ describe('TaskForm Component', () => {
100
128
  return render(_jsx(TaskForm, { ...defaultProps, ...props }));
101
129
  };
102
130
  // --- Create Task Tests ---
103
- it('3.1 Open add form - Renders correctly', () => {
131
+ it("3.1 Open add form - Renders correctly", () => {
104
132
  renderForm();
105
- expect(screen.getByPlaceholderText('What needs to be done?')).toBeInTheDocument();
106
- expect(screen.getByTitle('Markdown Help')).toBeInTheDocument();
133
+ expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
134
+ expect(screen.getByTitle("Markdown Help")).toBeInTheDocument();
107
135
  });
108
- it('3.2 Title required - Submit disabled if empty', () => {
109
- renderForm({ title: '' });
136
+ it("3.2 Title required - Submit disabled if empty", () => {
137
+ renderForm({ title: "" });
110
138
  // In the implementation, the form submit handles validation, but let's check input attribute
111
- const input = screen.getByPlaceholderText('What needs to be done?');
139
+ const input = screen.getByPlaceholderText("What needs to be done?");
112
140
  expect(input).toBeRequired();
113
141
  });
114
- it('3.3 Create minimal - Title input updates', async () => {
142
+ it("3.3 Create minimal - Title input updates", async () => {
115
143
  const user = userEvent.setup();
116
144
  renderForm();
117
- const input = screen.getByPlaceholderText('What needs to be done?');
118
- await user.type(input, 'New Task');
145
+ const input = screen.getByPlaceholderText("What needs to be done?");
146
+ await user.type(input, "New Task");
119
147
  expect(defaultProps.onTitleChange).toHaveBeenCalled();
120
148
  });
121
- it('3.4 Create with all fields - Description updates', async () => {
149
+ it("3.4 Create with all fields - Description updates", async () => {
122
150
  const user = userEvent.setup();
123
151
  renderForm({ descriptionFocused: true }); // Ensure textarea is rendered
124
- const textarea = screen.getByPlaceholderText('What needs to be done? (Markdown supported)');
125
- await user.type(textarea, 'Details');
152
+ const textarea = screen.getByPlaceholderText("What needs to be done? (Markdown supported)");
153
+ await user.type(textarea, "Details");
126
154
  expect(defaultProps.onDescriptionChange).toHaveBeenCalled();
127
155
  });
128
- it('renders actual newline paragraphs in description preview without escaped text', () => {
156
+ it("renders actual newline paragraphs in description preview without escaped text", () => {
129
157
  const { container } = renderForm({
130
- description: 'Phase 1\n\nPhase 2',
131
- descriptionFocused: false
158
+ description: "Phase 1\n\nPhase 2",
159
+ descriptionFocused: false,
132
160
  });
133
- expect(screen.getByText('Phase 1')).toBeInTheDocument();
134
- expect(screen.getByText('Phase 2')).toBeInTheDocument();
135
- expect(container.textContent).not.toContain('\\n\\n');
161
+ expect(screen.getByText("Phase 1")).toBeInTheDocument();
162
+ expect(screen.getByText("Phase 2")).toBeInTheDocument();
163
+ expect(container.textContent).not.toContain("\\n\\n");
136
164
  });
137
- it('3.5 Category selection', async () => {
165
+ it("3.5 Category selection", async () => {
138
166
  const user = userEvent.setup();
139
167
  renderForm();
140
- const dropdown = screen.getByRole('combobox', { name: /category/i });
168
+ const dropdown = screen.getByRole("combobox", { name: /category/i });
141
169
  await user.click(dropdown);
142
- const option = screen.getByText('Backend');
170
+ const option = screen.getByText("Backend");
143
171
  await user.click(option);
144
- expect(defaultProps.onCategoryChange).toHaveBeenCalledWith('backend');
172
+ expect(defaultProps.onCategoryChange).toHaveBeenCalledWith("backend");
145
173
  });
146
- it('keeps a legacy category selectable when the current task already uses it', async () => {
174
+ it("keeps a legacy category selectable when the current task already uses it", async () => {
147
175
  const user = userEvent.setup();
148
176
  renderForm({
149
- category: 'backend',
177
+ category: "backend",
150
178
  categories: [
151
- { value: 'default', label: 'General', disabled: false },
152
- { value: 'backend', label: 'Backend', disabled: true }
153
- ]
179
+ { value: "default", label: "General", disabled: false },
180
+ { value: "backend", label: "Backend", disabled: true },
181
+ ],
154
182
  });
155
- const dropdown = screen.getByRole('combobox', { name: /category/i });
183
+ const dropdown = screen.getByRole("combobox", { name: /category/i });
156
184
  await user.click(dropdown);
157
- expect(screen.getAllByText('Backend (Legacy)').length).toBeGreaterThan(0);
185
+ expect(screen.getAllByText("Backend (Legacy)").length).toBeGreaterThan(0);
158
186
  });
159
- it('uses the configured default category label in the category picker', async () => {
187
+ it("uses the configured default category label in the category picker", async () => {
160
188
  const user = userEvent.setup();
161
189
  renderForm({
162
190
  categories: [
163
- { value: 'default', label: 'Inbox Work', icon: 'Star', color: 'teal-500', disabled: false },
164
- { value: 'backend', label: 'Backend', disabled: false }
165
- ]
191
+ {
192
+ value: "default",
193
+ label: "Inbox Work",
194
+ icon: "Star",
195
+ color: "teal-500",
196
+ disabled: false,
197
+ },
198
+ { value: "backend", label: "Backend", disabled: false },
199
+ ],
166
200
  });
167
- const dropdown = screen.getByRole('combobox', { name: /category/i });
201
+ const dropdown = screen.getByRole("combobox", { name: /category/i });
168
202
  await user.click(dropdown);
169
- expect(screen.getByText('Inbox Work')).toBeInTheDocument();
170
- expect(screen.queryByText('General')).not.toBeInTheDocument();
203
+ expect(screen.getByText("Inbox Work")).toBeInTheDocument();
204
+ expect(screen.queryByText("General")).not.toBeInTheDocument();
171
205
  });
172
- it('3.6 Type selection', async () => {
206
+ it("3.6 Type selection", async () => {
173
207
  const user = userEvent.setup();
174
208
  renderForm();
175
- const dropdown = screen.getByRole('combobox', { name: /type/i });
209
+ const dropdown = screen.getByRole("combobox", { name: /type/i });
176
210
  await user.click(dropdown);
177
- const option = screen.getByText('Bug');
211
+ const option = screen.getByText("Bug");
178
212
  await user.click(option);
179
- expect(defaultProps.onTypeChange).toHaveBeenCalledWith('bug');
213
+ expect(defaultProps.onTypeChange).toHaveBeenCalledWith("bug");
180
214
  });
181
- it('keeps a retired type selectable when the current task already uses it', async () => {
215
+ it("keeps a retired type selectable when the current task already uses it", async () => {
182
216
  const user = userEvent.setup();
183
217
  renderForm({
184
- type: 'issue',
218
+ type: "issue",
185
219
  types: [
186
- { value: 'task', label: 'Task', status: 'active' },
187
- { value: 'issue', label: 'Issue', status: 'retired' }
188
- ]
220
+ { value: "task", label: "Task", status: "active" },
221
+ { value: "issue", label: "Issue", status: "retired" },
222
+ ],
189
223
  });
190
- const dropdown = screen.getByRole('combobox', { name: /type/i });
224
+ const dropdown = screen.getByRole("combobox", { name: /type/i });
191
225
  await user.click(dropdown);
192
- expect(screen.getAllByText('Issue (Retired)').length).toBeGreaterThan(0);
226
+ expect(screen.getAllByText("Issue (Retired)").length).toBeGreaterThan(0);
193
227
  });
194
- it('3.7 Priority selection', () => {
228
+ it("3.7 Priority selection", () => {
195
229
  renderForm();
196
230
  // LevelSelector uses divs/buttons. Looking at LevelSelector implementation (implied):
197
231
  // It renders options. Let's click one.
198
232
  // Priority and Complexity both have 'High' options.
199
- const priorityOption = screen.getAllByTitle('High')[0];
233
+ const priorityOption = screen.getAllByTitle("High")[0];
200
234
  fireEvent.click(priorityOption);
201
235
  expect(defaultProps.onPriorityChange).toHaveBeenCalledWith(3);
202
236
  });
203
- it('renders workspace member assignee options', async () => {
237
+ it("renders workspace member assignee options", async () => {
204
238
  const user = userEvent.setup();
205
239
  renderForm();
206
- const dropdown = screen.getByRole('combobox', { name: /assigned to/i });
240
+ const dropdown = screen.getByRole("combobox", { name: /assigned to/i });
207
241
  await user.click(dropdown);
208
- expect(screen.getByText('Alex Example')).toBeInTheDocument();
242
+ expect(screen.getByText("Alex Example")).toBeInTheDocument();
209
243
  });
210
- it('shows attach to workstream only on the new task form', () => {
244
+ it("shows attach to workstream only on the new task form", () => {
211
245
  renderForm();
212
- expect(screen.getByText('Attach to workstream')).toBeInTheDocument();
213
- expect(screen.getByRole('combobox', { name: /attach to workstream/i })).toBeInTheDocument();
246
+ expect(screen.getByText("Attach to workstream")).toBeInTheDocument();
247
+ expect(screen.getByRole("combobox", { name: /attach to workstream/i })).toBeInTheDocument();
214
248
  });
215
- it('does not show attach to workstream on the edit task form', () => {
249
+ it("does not show attach to workstream on the edit task form", () => {
216
250
  renderForm({
217
- editingTaskId: 'task-1',
251
+ editingTaskId: "task-1",
218
252
  currentTask: {
219
- id: 'task-1',
220
- title: 'Existing task',
221
- description: '',
222
- category: 'general',
223
- type: 'feature',
253
+ id: "task-1",
254
+ title: "Existing task",
255
+ description: "",
256
+ category: "general",
257
+ type: "feature",
224
258
  priority: 2,
225
- status: 'task',
226
- createdAt: '2026-04-03T00:00:00.000Z',
227
- }
259
+ status: "task",
260
+ createdAt: "2026-04-03T00:00:00.000Z",
261
+ },
228
262
  });
229
- expect(screen.queryByText('Attach to workstream')).not.toBeInTheDocument();
263
+ expect(screen.queryByText("Attach to workstream")).not.toBeInTheDocument();
230
264
  });
231
- it('switches the create-workstream control into manual entry when plus is clicked', async () => {
265
+ it("switches the create-workstream control into manual entry when plus is clicked", async () => {
232
266
  const user = userEvent.setup();
233
267
  renderForm();
234
- await user.click(screen.getByRole('button', { name: /enter workstream by name/i }));
235
- const manualInput = screen.getByRole('textbox', { name: /attach to workstream/i });
268
+ await user.click(screen.getByRole("button", { name: /enter workstream by name/i }));
269
+ const manualInput = screen.getByRole("textbox", {
270
+ name: /attach to workstream/i,
271
+ });
236
272
  expect(manualInput).toBeInTheDocument();
237
- await user.type(manualInput, 'Platform Refinement');
273
+ await user.type(manualInput, "Platform Refinement");
238
274
  expect(defaultProps.onWorkstreamInputChange).toHaveBeenCalled();
239
275
  });
240
- it('renders custom taxonomy descriptions as helper text', () => {
276
+ it("renders custom taxonomy descriptions as helper text", () => {
241
277
  renderForm({
242
- taxonomies: [{
243
- id: 'review-status',
244
- label: 'Review Status',
245
- description: 'Use this to track review routing.',
246
- widgetType: 'select',
278
+ taxonomies: [
279
+ {
280
+ id: "review-status",
281
+ label: "Review Status",
282
+ description: "Use this to track review routing.",
283
+ widgetType: "select",
247
284
  isSystem: false,
248
285
  multiSelect: false,
249
- options: [{ value: 'pending', label: 'Pending', status: 'active' }]
250
- }]
286
+ options: [{ value: "pending", label: "Pending", status: "active" }],
287
+ },
288
+ ],
251
289
  });
252
- expect(screen.getByText('Use this to track review routing.')).toBeInTheDocument();
290
+ expect(screen.getByText("Use this to track review routing.")).toBeInTheDocument();
253
291
  });
254
- it('hides custom taxonomies from the task form when form visibility is disabled', () => {
292
+ it("hides custom taxonomies from the task form when form visibility is disabled", () => {
255
293
  renderForm({
256
- taxonomies: [{
257
- id: 'review-status',
258
- label: 'Review Status',
294
+ taxonomies: [
295
+ {
296
+ id: "review-status",
297
+ label: "Review Status",
259
298
  formEnabled: false,
260
- widgetType: 'select',
299
+ widgetType: "select",
261
300
  isSystem: false,
262
301
  multiSelect: false,
263
- options: [{ value: 'pending', label: 'Pending', status: 'active' }]
264
- }]
302
+ options: [{ value: "pending", label: "Pending", status: "active" }],
303
+ },
304
+ ],
265
305
  });
266
- expect(screen.queryByText('Review Status')).not.toBeInTheDocument();
306
+ expect(screen.queryByText("Review Status")).not.toBeInTheDocument();
267
307
  });
268
- it('keeps a hidden custom taxonomy visible when the current task already has a value', () => {
308
+ it("keeps a hidden custom taxonomy visible when the current task already has a value", () => {
269
309
  renderForm({
270
- formTaxonomies: { 'review-status': 'pending' },
271
- taxonomies: [{
272
- id: 'review-status',
273
- label: 'Review Status',
310
+ formTaxonomies: { "review-status": "pending" },
311
+ taxonomies: [
312
+ {
313
+ id: "review-status",
314
+ label: "Review Status",
274
315
  formEnabled: false,
275
- widgetType: 'select',
316
+ widgetType: "select",
276
317
  isSystem: false,
277
318
  multiSelect: false,
278
- options: [{ value: 'pending', label: 'Pending', status: 'active' }]
279
- }]
319
+ options: [{ value: "pending", label: "Pending", status: "active" }],
320
+ },
321
+ ],
280
322
  });
281
- expect(screen.getByText('Review Status')).toBeInTheDocument();
323
+ expect(screen.getByText("Review Status")).toBeInTheDocument();
282
324
  });
283
- it('renders multi-select custom taxonomies as selectable chips', async () => {
325
+ it("renders multi-select custom taxonomies as selectable chips", async () => {
284
326
  const user = userEvent.setup();
285
327
  renderForm({
286
- taxonomies: [{
287
- id: 'channels',
288
- label: 'Channels',
289
- widgetType: 'select',
328
+ taxonomies: [
329
+ {
330
+ id: "channels",
331
+ label: "Channels",
332
+ widgetType: "select",
290
333
  isSystem: false,
291
334
  multiSelect: true,
292
335
  options: [
293
- { value: 'email', label: 'Email', status: 'active' },
294
- { value: 'social', label: 'Social', status: 'active' }
295
- ]
296
- }]
336
+ { value: "email", label: "Email", status: "active" },
337
+ { value: "social", label: "Social", status: "active" },
338
+ ],
339
+ },
340
+ ],
297
341
  });
298
- await user.click(screen.getByRole('button', { name: 'Email' }));
299
- expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith('channels', ['email']);
342
+ await user.click(screen.getByRole("button", { name: "Email" }));
343
+ expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith("channels", [
344
+ "email",
345
+ ]);
300
346
  });
301
- it('keeps selected retired multi-select custom taxonomy options available', () => {
347
+ it("keeps selected retired multi-select custom taxonomy options available", () => {
302
348
  renderForm({
303
- formTaxonomies: { channels: ['email'] },
304
- taxonomies: [{
305
- id: 'channels',
306
- label: 'Channels',
307
- widgetType: 'select',
349
+ formTaxonomies: { channels: ["email"] },
350
+ taxonomies: [
351
+ {
352
+ id: "channels",
353
+ label: "Channels",
354
+ widgetType: "select",
308
355
  isSystem: false,
309
356
  multiSelect: true,
310
357
  options: [
311
- { value: 'email', label: 'Email', status: 'retired' },
312
- { value: 'social', label: 'Social', status: 'active' }
313
- ]
314
- }]
358
+ { value: "email", label: "Email", status: "retired" },
359
+ { value: "social", label: "Social", status: "active" },
360
+ ],
361
+ },
362
+ ],
315
363
  });
316
- expect(screen.getByRole('button', { name: /Email \(Retired\)/i })).toBeInTheDocument();
317
- expect(screen.getByRole('button', { name: 'Social' })).toBeInTheDocument();
364
+ expect(screen.getByRole("button", { name: /Email \(Retired\)/i })).toBeInTheDocument();
365
+ expect(screen.getByRole("button", { name: "Social" })).toBeInTheDocument();
318
366
  });
319
- it('hides retired custom taxonomies from new-task entry when there is no selected value', () => {
367
+ it("hides retired custom taxonomies from new-task entry when there is no selected value", () => {
320
368
  renderForm({
321
- taxonomies: [{
322
- id: 'channels',
323
- label: 'Channels',
324
- status: 'retired',
325
- widgetType: 'select',
369
+ taxonomies: [
370
+ {
371
+ id: "channels",
372
+ label: "Channels",
373
+ status: "retired",
374
+ widgetType: "select",
326
375
  isSystem: false,
327
376
  multiSelect: false,
328
- options: [{ value: 'email', label: 'Email', status: 'active' }]
329
- }]
377
+ options: [{ value: "email", label: "Email", status: "active" }],
378
+ },
379
+ ],
330
380
  });
331
- expect(screen.queryByText('Channels')).not.toBeInTheDocument();
381
+ expect(screen.queryByText("Channels")).not.toBeInTheDocument();
332
382
  });
333
- it('keeps retired custom taxonomies visible when the current task already has a value', () => {
383
+ it("keeps retired custom taxonomies visible when the current task already has a value", () => {
334
384
  renderForm({
335
- formTaxonomies: { channels: 'email' },
336
- taxonomies: [{
337
- id: 'channels',
338
- label: 'Channels',
339
- status: 'retired',
340
- widgetType: 'select',
385
+ formTaxonomies: { channels: "email" },
386
+ taxonomies: [
387
+ {
388
+ id: "channels",
389
+ label: "Channels",
390
+ status: "retired",
391
+ widgetType: "select",
341
392
  isSystem: false,
342
393
  multiSelect: false,
343
- options: [{ value: 'email', label: 'Email', status: 'active' }]
344
- }]
394
+ options: [{ value: "email", label: "Email", status: "active" }],
395
+ },
396
+ ],
345
397
  });
346
- expect(screen.getByText('Channels')).toBeInTheDocument();
398
+ expect(screen.getByText("Channels")).toBeInTheDocument();
347
399
  });
348
400
  // --- Edit Task Tests ---
349
- it('3.10 Load existing task', () => {
350
- renderForm({ editingTaskId: '123', title: 'Existing Task' });
351
- expect(screen.getByDisplayValue('Existing Task')).toBeInTheDocument();
401
+ it("3.10 Load existing task", () => {
402
+ renderForm({ editingTaskId: "123", title: "Existing Task" });
403
+ expect(screen.getByDisplayValue("Existing Task")).toBeInTheDocument();
352
404
  });
353
- it('3.11 ID badge visible in edit mode', () => {
405
+ it("3.11 ID badge visible in edit mode", () => {
354
406
  // Form-level edit mode validation.
355
- renderForm({ editingTaskId: '123' });
407
+ renderForm({ editingTaskId: "123" });
356
408
  expect(screen.getByText(/Activity & Comments/)).toBeInTheDocument();
357
409
  });
358
- it('3.12 Copy ID', async () => {
410
+ it("3.12 Copy ID", async () => {
359
411
  // Header copy action is outside TaskForm; keep form render sanity check.
360
412
  renderForm();
361
- expect(screen.getByPlaceholderText('What needs to be done?')).toBeInTheDocument();
413
+ expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
362
414
  });
363
- it('renders assignee audit activity with readable labels instead of raw ids', () => {
415
+ it("renders assignee audit activity with readable labels instead of raw ids", () => {
364
416
  renderForm({
365
- editingTaskId: '123',
417
+ editingTaskId: "123",
366
418
  showComments: true,
367
419
  assigneeOptions: [
368
420
  ...defaultProps.assigneeOptions,
369
- { value: 'ai-profile-1', label: 'Planner Bot', icon: 'Bot', color: '#8b5cf6', kind: 'agent' },
421
+ {
422
+ value: "ai-profile-1",
423
+ label: "Planner Bot",
424
+ icon: "Bot",
425
+ color: "#8b5cf6",
426
+ kind: "agent",
427
+ },
370
428
  ],
371
429
  currentTask: {
372
- id: '123',
373
- title: 'Existing Task',
430
+ id: "123",
431
+ title: "Existing Task",
374
432
  activity: [
375
433
  {
376
- id: 'event:task-event-1',
377
- type: 'event',
378
- timestamp: '2026-03-23T12:00:00.000Z',
434
+ id: "event:task-event-1",
435
+ type: "event",
436
+ timestamp: "2026-03-23T12:00:00.000Z",
379
437
  event: {
380
- id: 'task-event-1',
381
- taskId: '123',
382
- workspaceId: 'workspace-1',
383
- action: 'task-updated',
384
- actor: 'member-1',
385
- actorType: 'human',
386
- createdAt: '2026-03-23T12:00:00.000Z',
438
+ id: "task-event-1",
439
+ taskId: "123",
440
+ workspaceId: "workspace-1",
441
+ action: "task-updated",
442
+ actor: "member-1",
443
+ actorType: "human",
444
+ createdAt: "2026-03-23T12:00:00.000Z",
387
445
  details: {
388
446
  changes: {
389
447
  assignee: {
390
- from: 'unassigned',
391
- to: 'ai-profile-1',
448
+ from: "unassigned",
449
+ to: "ai-profile-1",
392
450
  },
393
451
  },
394
452
  },
@@ -397,52 +455,88 @@ describe('TaskForm Component', () => {
397
455
  ],
398
456
  },
399
457
  });
400
- expect(screen.getByText('ASSIGNED TO: Unassigned -> Planner Bot')).toBeInTheDocument();
458
+ expect(screen.getByText("ASSIGNED TO: Unassigned -> Planner Bot")).toBeInTheDocument();
401
459
  expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
402
460
  });
403
- it('3.13 Update title', async () => {
461
+ it("renders planning relationship audit activity with reference labels instead of raw ids", () => {
462
+ renderForm({
463
+ editingTaskId: "123",
464
+ showComments: true,
465
+ currentTask: {
466
+ id: "123",
467
+ title: "Existing Task",
468
+ activity: [
469
+ {
470
+ id: "event:task-event-relationship",
471
+ type: "event",
472
+ timestamp: "2026-03-23T12:00:00.000Z",
473
+ event: {
474
+ id: "task-event-relationship",
475
+ taskId: "123",
476
+ workspaceId: "workspace-1",
477
+ action: "task-relationship-changed",
478
+ actor: "member-1",
479
+ actorType: "human",
480
+ createdAt: "2026-03-23T12:00:00.000Z",
481
+ details: {
482
+ changes: {
483
+ workstreamId: {
484
+ from: null,
485
+ to: "workstream-1",
486
+ },
487
+ },
488
+ },
489
+ },
490
+ },
491
+ ],
492
+ },
493
+ });
494
+ expect(screen.getByText("Workstream: none -> WS-12")).toBeInTheDocument();
495
+ expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
496
+ });
497
+ it("3.13 Update title", async () => {
404
498
  const user = userEvent.setup();
405
- renderForm({ editingTaskId: '123', title: 'Old Title' });
406
- const input = screen.getByDisplayValue('Old Title');
499
+ renderForm({ editingTaskId: "123", title: "Old Title" });
500
+ const input = screen.getByDisplayValue("Old Title");
407
501
  await user.clear(input);
408
- await user.type(input, 'New Title');
502
+ await user.type(input, "New Title");
409
503
  expect(defaultProps.onTitleChange).toHaveBeenCalled();
410
504
  });
411
- it('keeps scheduled date editable in the task form', () => {
505
+ it("keeps scheduled date editable in the task form", () => {
412
506
  renderForm({
413
- editingTaskId: 'parent-1',
414
- currentTask: { id: 'parent-1', title: 'Parent Task' },
507
+ editingTaskId: "parent-1",
508
+ currentTask: { id: "parent-1", title: "Parent Task" },
415
509
  });
416
- const scheduledLabel = screen.getByText('Scheduled:');
510
+ const scheduledLabel = screen.getByText("Scheduled:");
417
511
  const scheduledInput = scheduledLabel.nextElementSibling;
418
512
  expect(scheduledInput).toBeTruthy();
419
513
  if (!scheduledInput)
420
- throw new Error('Scheduled date input missing');
514
+ throw new Error("Scheduled date input missing");
421
515
  expect(scheduledInput.disabled).toBe(false);
422
- expect(screen.queryByText('Parent tasks cannot be scheduled. Schedule leaf tasks instead.')).not.toBeInTheDocument();
516
+ expect(screen.queryByText("Parent tasks cannot be scheduled. Schedule leaf tasks instead.")).not.toBeInTheDocument();
423
517
  });
424
- it('3.14 Context Documents section renders', async () => {
518
+ it("3.14 Context Documents section renders", async () => {
425
519
  const user = userEvent.setup();
426
520
  renderForm();
427
- expect(screen.getByText('Context Documents')).toBeInTheDocument();
428
- await user.click(screen.getByTitle('Show context documents'));
429
- expect(screen.getByRole('button', { name: /upload/i })).toBeInTheDocument();
521
+ expect(screen.getByText("Context Documents")).toBeInTheDocument();
522
+ await user.click(screen.getByTitle("Show context documents"));
523
+ expect(screen.getByRole("button", { name: /upload/i })).toBeInTheDocument();
430
524
  });
431
- it('3.15 Markdown Help Toggle', async () => {
525
+ it("3.15 Markdown Help Toggle", async () => {
432
526
  const user = userEvent.setup();
433
527
  renderForm();
434
- const helpBtn = screen.getByTitle('Markdown Help');
528
+ const helpBtn = screen.getByTitle("Markdown Help");
435
529
  await user.click(helpBtn);
436
530
  expect(defaultProps.onShowMarkdownHelpChange).toHaveBeenCalledWith(true);
437
531
  });
438
- it('3.16 Description Focus', () => {
532
+ it("3.16 Description Focus", () => {
439
533
  renderForm();
440
- renderForm({ description: 'Test Desc', descriptionFocused: false });
441
- const previewDiv = screen.getByText('Test Desc');
534
+ renderForm({ description: "Test Desc", descriptionFocused: false });
535
+ const previewDiv = screen.getByText("Test Desc");
442
536
  fireEvent.click(previewDiv);
443
537
  expect(defaultProps.onDescriptionFocusedChange).toHaveBeenCalledWith(true);
444
538
  });
445
- it('3.18 Category Manager Toggle', async () => {
539
+ it("3.18 Category Manager Toggle", async () => {
446
540
  const user = userEvent.setup();
447
541
  renderForm();
448
542
  // The + button next to Category label
@@ -471,252 +565,279 @@ describe('TaskForm Component', () => {
471
565
  const { container } = renderForm();
472
566
  // Select the first button with class manageLink (assuming style imports work or we use classname)
473
567
  // Styles are imported as object `styles`. In test, classes are mocked or stringified?
474
- // Vitest CSS handling usually preserves class names if using `identity-obj-proxy` or similar,
568
+ // Vitest CSS handling usually preserves class names if using `identity-obj-proxy` or similar,
475
569
  // but here we have `css: true` in vitest config.
476
570
  // It might be hashed.
477
571
  // Fallback: The button contains an SVG.
478
572
  // Let's try to find the button by its visual role.
479
573
  // It is `tabIndex={-1}`.
480
- const buttons = screen.getAllByRole('button', { hidden: true }); // tabIndex -1 might hide it from accessibility tree? No.
574
+ const buttons = screen.getAllByRole("button", { hidden: true }); // tabIndex -1 might hide it from accessibility tree? No.
481
575
  // Let's skip this specific test for now or try to match by association.
482
576
  expect(true).toBe(true);
483
577
  });
484
578
  // --- Unsaved Changes Tests ---
485
579
  // These logic reside in the Parent (Taskforce), checking state vs original.
486
- // TaskForm just calls `onChange`.
580
+ // TaskForm just calls `onChange`.
487
581
  // So "Test 3.19 Trigger warning" is an INTEGRATION test for Taskforce, not Unit test for TaskForm.
488
582
  // But we CAN verify that TaskForm calls the change handlers correctly, which we did in 3.3, 3.4.
489
583
  // --- Comments ---
490
- it('3.23 Expand comments', async () => {
491
- const user = userEvent.setup();
492
- renderForm({ editingTaskId: '123' });
493
- const toggle = screen.getByTitle('Show Comments');
494
- await user.click(toggle);
495
- expect(defaultProps.onShowCommentsChange).toHaveBeenCalledWith(true);
584
+ it("3.23 Shows the activity sidebar in edit mode", () => {
585
+ renderForm({ editingTaskId: "123" });
586
+ expect(screen.getByText("Activity & Comments")).toBeInTheDocument();
587
+ expect(screen.getByPlaceholderText("Type a comment...")).toBeInTheDocument();
588
+ });
589
+ it("scrolls the activity thread to the latest item on open", () => {
590
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
591
+ const scrollIntoView = vi.fn();
592
+ Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
593
+ configurable: true,
594
+ value: scrollIntoView,
595
+ });
596
+ try {
597
+ renderForm({
598
+ editingTaskId: "123",
599
+ comments: [
600
+ {
601
+ id: "comment-scroll-1",
602
+ author: "member-1",
603
+ text: "Latest comment",
604
+ timestamp: "2026-03-22T10:10:00.000Z",
605
+ },
606
+ ],
607
+ });
608
+ expect(scrollIntoView).toHaveBeenCalled();
609
+ }
610
+ finally {
611
+ if (originalScrollIntoView) {
612
+ Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
613
+ configurable: true,
614
+ value: originalScrollIntoView,
615
+ });
616
+ }
617
+ else {
618
+ delete HTMLElement.prototype.scrollIntoView;
619
+ }
620
+ }
496
621
  });
497
- it('3.24 Add comment', async () => {
622
+ it("3.24 Add comment", async () => {
498
623
  const user = userEvent.setup();
499
- renderForm({ editingTaskId: '123', showComments: true, newCommentText: 'Nice work' });
500
- const sendBtn = screen.getByRole('button', { name: '' }); // Send icon has no text?
501
- // The button has <Send /> icon.
502
- // We can find by class or hierarchy.
503
- // Or find the textarea and the button next to it.
504
- const textarea = screen.getByPlaceholderText('Type a comment...');
505
- const button = textarea.nextSibling; // Assuming button is next sibling
506
- await user.click(button);
624
+ renderForm({ editingTaskId: "123", newCommentText: "Nice work" });
625
+ await user.click(screen.getByRole("button", { name: /send comment/i }));
507
626
  expect(defaultProps.onAddComment).toHaveBeenCalled();
508
627
  });
509
- it('renders named AI comment authors from resolved actor data', () => {
628
+ it("renders named AI comment authors from resolved actor data", () => {
510
629
  renderForm({
511
- editingTaskId: '123',
630
+ editingTaskId: "123",
512
631
  showComments: true,
513
632
  comments: [
514
633
  {
515
- id: 'comment-1',
516
- author: 'ai-profile-123',
517
- text: 'I reviewed the task details.',
518
- timestamp: '2026-03-22T10:00:00.000Z',
634
+ id: "comment-1",
635
+ author: "ai-profile-123",
636
+ text: "I reviewed the task details.",
637
+ timestamp: "2026-03-22T10:00:00.000Z",
519
638
  actor: {
520
- id: 'ai-profile-123',
521
- kind: 'ai',
522
- label: 'Codex',
523
- username: 'codex',
524
- icon: 'Bot',
525
- color: '#0ea5e9',
639
+ id: "ai-profile-123",
640
+ kind: "ai",
641
+ label: "Codex",
642
+ username: "codex",
643
+ icon: "Bot",
644
+ color: "#0ea5e9",
526
645
  isNamed: true,
527
646
  },
528
647
  },
529
648
  ],
530
649
  });
531
- expect(screen.getByText('Codex')).toBeInTheDocument();
532
- expect(screen.queryByText('AI Agent')).not.toBeInTheDocument();
650
+ expect(screen.getByText("Codex")).toBeInTheDocument();
651
+ expect(screen.queryByText("AI Agent")).not.toBeInTheDocument();
533
652
  });
534
- it('renders task audit events in the activity feed', () => {
653
+ it("renders task audit events in the activity feed", () => {
535
654
  renderForm({
536
- editingTaskId: '123',
655
+ editingTaskId: "123",
537
656
  showComments: true,
538
657
  currentTask: {
539
- id: 'task-activity-1',
540
- title: 'Activity Task',
541
- status: 'in-progress',
542
- createdAt: '2026-03-22T09:00:00.000Z',
658
+ id: "task-activity-1",
659
+ title: "Activity Task",
660
+ status: "in-progress",
661
+ createdAt: "2026-03-22T09:00:00.000Z",
543
662
  activity: [
544
663
  {
545
- id: 'event:1',
546
- type: 'event',
547
- timestamp: '2026-03-22T10:00:00.000Z',
664
+ id: "event:1",
665
+ type: "event",
666
+ timestamp: "2026-03-22T10:00:00.000Z",
548
667
  event: {
549
- id: 'event-1',
550
- taskId: 'task-activity-1',
551
- workspaceId: 'default',
552
- action: 'task-status-changed',
553
- actor: 'user',
554
- actorType: 'human',
555
- createdAt: '2026-03-22T10:00:00.000Z',
668
+ id: "event-1",
669
+ taskId: "task-activity-1",
670
+ workspaceId: "default",
671
+ action: "task-status-changed",
672
+ actor: "user",
673
+ actorType: "human",
674
+ createdAt: "2026-03-22T10:00:00.000Z",
556
675
  details: {
557
676
  changes: {
558
- status: { from: 'task', to: 'in-progress' }
559
- }
560
- }
561
- }
562
- }
563
- ]
677
+ status: { from: "task", to: "in-progress" },
678
+ },
679
+ },
680
+ },
681
+ },
682
+ ],
564
683
  },
565
684
  });
566
- expect(screen.getByText('Status: task -> in-progress')).toBeInTheDocument();
567
- expect(screen.queryByText('System')).not.toBeInTheDocument();
685
+ expect(screen.getByText("Status: task -> in-progress")).toBeInTheDocument();
686
+ expect(screen.queryByText("System")).not.toBeInTheDocument();
568
687
  });
569
- it('summarizes description audit events without rendering the full description body', () => {
688
+ it("summarizes description audit events without rendering the full description body", () => {
570
689
  renderForm({
571
- editingTaskId: '123',
690
+ editingTaskId: "123",
572
691
  showComments: true,
573
692
  currentTask: {
574
- id: 'task-activity-2',
575
- title: 'Description Audit Task',
576
- status: 'task',
577
- createdAt: '2026-03-22T09:00:00.000Z',
693
+ id: "task-activity-2",
694
+ title: "Description Audit Task",
695
+ status: "task",
696
+ createdAt: "2026-03-22T09:00:00.000Z",
578
697
  activity: [
579
698
  {
580
- id: 'event:2',
581
- type: 'event',
582
- timestamp: '2026-03-22T10:00:00.000Z',
699
+ id: "event:2",
700
+ type: "event",
701
+ timestamp: "2026-03-22T10:00:00.000Z",
583
702
  event: {
584
- id: 'event-2',
585
- taskId: 'task-activity-2',
586
- workspaceId: 'default',
587
- action: 'task-updated',
588
- actor: 'user',
589
- actorType: 'human',
590
- createdAt: '2026-03-22T10:00:00.000Z',
703
+ id: "event-2",
704
+ taskId: "task-activity-2",
705
+ workspaceId: "default",
706
+ action: "task-updated",
707
+ actor: "user",
708
+ actorType: "human",
709
+ createdAt: "2026-03-22T10:00:00.000Z",
591
710
  details: {
592
711
  changes: {
593
712
  description: {
594
- from: '',
595
- to: 'Line one\n\nLine two\n\nLine three'
596
- }
597
- }
598
- }
599
- }
600
- }
601
- ]
713
+ from: "",
714
+ to: "Line one\n\nLine two\n\nLine three",
715
+ },
716
+ },
717
+ },
718
+ },
719
+ },
720
+ ],
602
721
  },
603
722
  });
604
- expect(screen.getByText('DESCRIPTION: added')).toBeInTheDocument();
723
+ expect(screen.getByText("DESCRIPTION: added")).toBeInTheDocument();
605
724
  expect(screen.queryByText(/Line one/)).not.toBeInTheDocument();
606
725
  expect(screen.queryByText(/Line two/)).not.toBeInTheDocument();
607
726
  expect(screen.queryByText(/Line three/)).not.toBeInTheDocument();
608
727
  });
609
- it('summarizes title audit events without rendering before and after title text', () => {
728
+ it("summarizes title audit events without rendering before and after title text", () => {
610
729
  renderForm({
611
- editingTaskId: '123',
730
+ editingTaskId: "123",
612
731
  showComments: true,
613
732
  currentTask: {
614
- id: 'task-activity-3',
615
- title: 'Renamed task',
616
- status: 'task',
617
- createdAt: '2026-03-22T09:00:00.000Z',
733
+ id: "task-activity-3",
734
+ title: "Renamed task",
735
+ status: "task",
736
+ createdAt: "2026-03-22T09:00:00.000Z",
618
737
  activity: [
619
738
  {
620
- id: 'event:3',
621
- type: 'event',
622
- timestamp: '2026-03-22T10:00:00.000Z',
739
+ id: "event:3",
740
+ type: "event",
741
+ timestamp: "2026-03-22T10:00:00.000Z",
623
742
  event: {
624
- id: 'event-3',
625
- taskId: 'task-activity-3',
626
- workspaceId: 'default',
627
- action: 'task-updated',
628
- actor: 'user',
629
- actorType: 'human',
630
- createdAt: '2026-03-22T10:00:00.000Z',
743
+ id: "event-3",
744
+ taskId: "task-activity-3",
745
+ workspaceId: "default",
746
+ action: "task-updated",
747
+ actor: "user",
748
+ actorType: "human",
749
+ createdAt: "2026-03-22T10:00:00.000Z",
631
750
  details: {
632
751
  changes: {
633
752
  title: {
634
- from: 'Old title',
635
- to: 'New title'
636
- }
637
- }
638
- }
639
- }
640
- }
641
- ]
753
+ from: "Old title",
754
+ to: "New title",
755
+ },
756
+ },
757
+ },
758
+ },
759
+ },
760
+ ],
642
761
  },
643
762
  });
644
- expect(screen.getByText('TITLE: updated')).toBeInTheDocument();
645
- expect(screen.queryByText('Old title')).not.toBeInTheDocument();
646
- expect(screen.queryByText('New title')).not.toBeInTheDocument();
763
+ expect(screen.getByText("TITLE: updated")).toBeInTheDocument();
764
+ expect(screen.queryByText("Old title")).not.toBeInTheDocument();
765
+ expect(screen.queryByText("New title")).not.toBeInTheDocument();
647
766
  });
648
- it('falls back to the current task actor label for AI comments when only the id is present', () => {
767
+ it("falls back to the current task actor label for AI comments when only the id is present", () => {
649
768
  renderForm({
650
- editingTaskId: '123',
769
+ editingTaskId: "123",
651
770
  showComments: true,
652
771
  currentTask: {
653
- id: '123',
654
- title: 'Codex-owned task',
655
- assignee: 'ai-profile-123',
772
+ id: "123",
773
+ title: "Codex-owned task",
774
+ assignee: "ai-profile-123",
656
775
  assigneeActor: {
657
- id: 'ai-profile-123',
658
- kind: 'ai',
659
- label: 'Codex',
660
- username: 'codex',
661
- icon: 'Bot',
662
- color: '#0ea5e9',
776
+ id: "ai-profile-123",
777
+ kind: "ai",
778
+ label: "Codex",
779
+ username: "codex",
780
+ icon: "Bot",
781
+ color: "#0ea5e9",
663
782
  isNamed: true,
664
783
  },
665
784
  },
666
785
  comments: [
667
786
  {
668
- id: 'comment-2',
669
- author: 'ai-profile-123',
670
- text: 'Rendered from task actor fallback.',
671
- timestamp: '2026-03-22T10:05:00.000Z',
787
+ id: "comment-2",
788
+ author: "ai-profile-123",
789
+ text: "Rendered from task actor fallback.",
790
+ timestamp: "2026-03-22T10:05:00.000Z",
672
791
  },
673
792
  ],
674
793
  });
675
- expect(screen.getByText('Codex')).toBeInTheDocument();
794
+ expect(screen.getByText("Codex")).toBeInTheDocument();
676
795
  expect(screen.queryByText(/019d|ai-profile/i)).not.toBeInTheDocument();
677
796
  });
678
- it('uses the resolved actor icon and color tint for named AI comments', () => {
797
+ it("uses the resolved actor icon and color tint for named AI comments", () => {
679
798
  const { container } = renderForm({
680
- editingTaskId: '123',
799
+ editingTaskId: "123",
681
800
  showComments: true,
682
801
  comments: [
683
802
  {
684
- id: 'comment-3',
685
- author: 'claude',
686
- text: 'Resolved actor presentation.',
687
- timestamp: '2026-03-22T10:10:00.000Z',
803
+ id: "comment-3",
804
+ author: "claude",
805
+ text: "Resolved actor presentation.",
806
+ timestamp: "2026-03-22T10:10:00.000Z",
688
807
  actor: {
689
- id: 'ai-profile-claude',
690
- kind: 'ai',
691
- label: 'Claude',
692
- username: 'claude',
693
- icon: 'Bot',
694
- color: '#f59e0b',
808
+ id: "ai-profile-claude",
809
+ kind: "ai",
810
+ label: "Claude",
811
+ username: "claude",
812
+ icon: "Bot",
813
+ color: "#f59e0b",
695
814
  isNamed: true,
696
815
  },
697
816
  },
698
817
  ],
699
818
  });
700
- expect(container.querySelector('.lucide-bot')).toBeTruthy();
701
- const bubble = screen.getByText('Resolved actor presentation.').closest('[class*="commentText"]');
819
+ expect(container.querySelector(".lucide-bot")).toBeTruthy();
820
+ const bubble = screen
821
+ .getByText("Resolved actor presentation.")
822
+ .closest('[class*="commentText"]');
702
823
  expect(bubble).toHaveStyle({
703
- borderColor: 'rgba(245, 158, 11, 0.35)',
824
+ borderColor: "rgba(245, 158, 11, 0.35)",
704
825
  });
705
826
  });
706
827
  // --- Context Upload ---
707
- it('3.25 Render context image', async () => {
828
+ it("3.25 Render context image", async () => {
708
829
  const user = userEvent.setup();
709
- renderForm({ contextFiles: [{ path: 'img.png', timestamp: '' }] });
710
- await user.click(screen.getByTitle('Show context documents'));
830
+ renderForm({ contextFiles: [{ path: "img.png", timestamp: "" }] });
831
+ await user.click(screen.getByTitle("Show context documents"));
711
832
  const img = screen.getByAltText(/Context file 0/i);
712
833
  expect(img).toBeInTheDocument();
713
- expect(img).toHaveAttribute('src', 'img.png');
834
+ expect(img).toHaveAttribute("src", "img.png");
714
835
  });
715
- it('3.26 Remove context file', async () => {
836
+ it("3.26 Remove context file", async () => {
716
837
  const user = userEvent.setup();
717
- renderForm({ contextFiles: [{ path: 'img.png', timestamp: '' }] });
718
- await user.click(screen.getByTitle('Show context documents'));
719
- const deleteBtn = screen.getByTitle('Delete');
838
+ renderForm({ contextFiles: [{ path: "img.png", timestamp: "" }] });
839
+ await user.click(screen.getByTitle("Show context documents"));
840
+ const deleteBtn = screen.getByTitle("Delete");
720
841
  await user.click(deleteBtn);
721
842
  expect(defaultProps.onRemoveContextFile).toHaveBeenCalledWith(0);
722
843
  });