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