@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.
- package/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +23 -63
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +104 -188
- package/dist/server/routes/billing.test.js +214 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- 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
|
|
3
|
-
import userEvent from
|
|
4
|
-
import { describe, it, expect, vi } from
|
|
5
|
-
import { TaskForm } from
|
|
6
|
-
describe(
|
|
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:
|
|
13
|
-
type:
|
|
9
|
+
error: "",
|
|
10
|
+
title: "",
|
|
11
|
+
description: "",
|
|
12
|
+
category: "general",
|
|
13
|
+
type: "feature",
|
|
14
14
|
priority: 2,
|
|
15
15
|
complexity: 3,
|
|
16
|
-
approach:
|
|
17
|
-
assignee:
|
|
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
|
-
{
|
|
36
|
-
|
|
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:
|
|
40
|
-
{ value:
|
|
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:
|
|
64
|
+
id: "workstream-1",
|
|
47
65
|
referenceNumber: 12,
|
|
48
|
-
referenceLabel:
|
|
49
|
-
title:
|
|
66
|
+
referenceLabel: "WS-12",
|
|
67
|
+
title: "Platform Refinement",
|
|
50
68
|
description: null,
|
|
51
|
-
createdAt:
|
|
69
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
52
70
|
},
|
|
53
71
|
{
|
|
54
|
-
id:
|
|
72
|
+
id: "workstream-2",
|
|
55
73
|
referenceNumber: 44,
|
|
56
|
-
referenceLabel:
|
|
57
|
-
title:
|
|
74
|
+
referenceLabel: "WS-44",
|
|
75
|
+
title: "Growth Experiments",
|
|
58
76
|
description: null,
|
|
59
|
-
createdAt:
|
|
77
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
60
78
|
},
|
|
61
79
|
],
|
|
62
80
|
initiatives: [
|
|
63
81
|
{
|
|
64
|
-
id:
|
|
82
|
+
id: "initiative-1",
|
|
65
83
|
referenceNumber: 3,
|
|
66
|
-
referenceLabel:
|
|
67
|
-
title:
|
|
84
|
+
referenceLabel: "IN-3",
|
|
85
|
+
title: "Execution model",
|
|
68
86
|
description: null,
|
|
69
|
-
createdAt:
|
|
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(
|
|
131
|
+
it("3.1 Open add form - Renders correctly", () => {
|
|
114
132
|
renderForm();
|
|
115
|
-
expect(screen.getByPlaceholderText(
|
|
116
|
-
expect(screen.getByTitle(
|
|
133
|
+
expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
|
|
134
|
+
expect(screen.getByTitle("Markdown Help")).toBeInTheDocument();
|
|
117
135
|
});
|
|
118
|
-
it(
|
|
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(
|
|
139
|
+
const input = screen.getByPlaceholderText("What needs to be done?");
|
|
122
140
|
expect(input).toBeRequired();
|
|
123
141
|
});
|
|
124
|
-
it(
|
|
142
|
+
it("3.3 Create minimal - Title input updates", async () => {
|
|
125
143
|
const user = userEvent.setup();
|
|
126
144
|
renderForm();
|
|
127
|
-
const input = screen.getByPlaceholderText(
|
|
128
|
-
await user.type(input,
|
|
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(
|
|
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(
|
|
135
|
-
await user.type(textarea,
|
|
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(
|
|
156
|
+
it("renders actual newline paragraphs in description preview without escaped text", () => {
|
|
139
157
|
const { container } = renderForm({
|
|
140
|
-
description:
|
|
141
|
-
descriptionFocused: false
|
|
158
|
+
description: "Phase 1\n\nPhase 2",
|
|
159
|
+
descriptionFocused: false,
|
|
142
160
|
});
|
|
143
|
-
expect(screen.getByText(
|
|
144
|
-
expect(screen.getByText(
|
|
145
|
-
expect(container.textContent).not.toContain(
|
|
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(
|
|
165
|
+
it("3.5 Category selection", async () => {
|
|
148
166
|
const user = userEvent.setup();
|
|
149
167
|
renderForm();
|
|
150
|
-
const dropdown = screen.getByRole(
|
|
168
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
151
169
|
await user.click(dropdown);
|
|
152
|
-
const option = screen.getByText(
|
|
170
|
+
const option = screen.getByText("Backend");
|
|
153
171
|
await user.click(option);
|
|
154
|
-
expect(defaultProps.onCategoryChange).toHaveBeenCalledWith(
|
|
172
|
+
expect(defaultProps.onCategoryChange).toHaveBeenCalledWith("backend");
|
|
155
173
|
});
|
|
156
|
-
it(
|
|
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:
|
|
177
|
+
category: "backend",
|
|
160
178
|
categories: [
|
|
161
|
-
{ value:
|
|
162
|
-
{ value:
|
|
163
|
-
]
|
|
179
|
+
{ value: "default", label: "General", disabled: false },
|
|
180
|
+
{ value: "backend", label: "Backend", disabled: true },
|
|
181
|
+
],
|
|
164
182
|
});
|
|
165
|
-
const dropdown = screen.getByRole(
|
|
183
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
166
184
|
await user.click(dropdown);
|
|
167
|
-
expect(screen.getAllByText(
|
|
185
|
+
expect(screen.getAllByText("Backend (Legacy)").length).toBeGreaterThan(0);
|
|
168
186
|
});
|
|
169
|
-
it(
|
|
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
|
-
{
|
|
174
|
-
|
|
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(
|
|
201
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
178
202
|
await user.click(dropdown);
|
|
179
|
-
expect(screen.getByText(
|
|
180
|
-
expect(screen.queryByText(
|
|
203
|
+
expect(screen.getByText("Inbox Work")).toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByText("General")).not.toBeInTheDocument();
|
|
181
205
|
});
|
|
182
|
-
it(
|
|
206
|
+
it("3.6 Type selection", async () => {
|
|
183
207
|
const user = userEvent.setup();
|
|
184
208
|
renderForm();
|
|
185
|
-
const dropdown = screen.getByRole(
|
|
209
|
+
const dropdown = screen.getByRole("combobox", { name: /type/i });
|
|
186
210
|
await user.click(dropdown);
|
|
187
|
-
const option = screen.getByText(
|
|
211
|
+
const option = screen.getByText("Bug");
|
|
188
212
|
await user.click(option);
|
|
189
|
-
expect(defaultProps.onTypeChange).toHaveBeenCalledWith(
|
|
213
|
+
expect(defaultProps.onTypeChange).toHaveBeenCalledWith("bug");
|
|
190
214
|
});
|
|
191
|
-
it(
|
|
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:
|
|
218
|
+
type: "issue",
|
|
195
219
|
types: [
|
|
196
|
-
{ value:
|
|
197
|
-
{ value:
|
|
198
|
-
]
|
|
220
|
+
{ value: "task", label: "Task", status: "active" },
|
|
221
|
+
{ value: "issue", label: "Issue", status: "retired" },
|
|
222
|
+
],
|
|
199
223
|
});
|
|
200
|
-
const dropdown = screen.getByRole(
|
|
224
|
+
const dropdown = screen.getByRole("combobox", { name: /type/i });
|
|
201
225
|
await user.click(dropdown);
|
|
202
|
-
expect(screen.getAllByText(
|
|
226
|
+
expect(screen.getAllByText("Issue (Retired)").length).toBeGreaterThan(0);
|
|
203
227
|
});
|
|
204
|
-
it(
|
|
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(
|
|
233
|
+
const priorityOption = screen.getAllByTitle("High")[0];
|
|
210
234
|
fireEvent.click(priorityOption);
|
|
211
235
|
expect(defaultProps.onPriorityChange).toHaveBeenCalledWith(3);
|
|
212
236
|
});
|
|
213
|
-
it(
|
|
237
|
+
it("renders workspace member assignee options", async () => {
|
|
214
238
|
const user = userEvent.setup();
|
|
215
239
|
renderForm();
|
|
216
|
-
const dropdown = screen.getByRole(
|
|
240
|
+
const dropdown = screen.getByRole("combobox", { name: /assigned to/i });
|
|
217
241
|
await user.click(dropdown);
|
|
218
|
-
expect(screen.getByText(
|
|
242
|
+
expect(screen.getByText("Alex Example")).toBeInTheDocument();
|
|
219
243
|
});
|
|
220
|
-
it(
|
|
244
|
+
it("shows attach to workstream only on the new task form", () => {
|
|
221
245
|
renderForm();
|
|
222
|
-
expect(screen.getByText(
|
|
223
|
-
expect(screen.getByRole(
|
|
246
|
+
expect(screen.getByText("Attach to workstream")).toBeInTheDocument();
|
|
247
|
+
expect(screen.getByRole("combobox", { name: /attach to workstream/i })).toBeInTheDocument();
|
|
224
248
|
});
|
|
225
|
-
it(
|
|
249
|
+
it("does not show attach to workstream on the edit task form", () => {
|
|
226
250
|
renderForm({
|
|
227
|
-
editingTaskId:
|
|
251
|
+
editingTaskId: "task-1",
|
|
228
252
|
currentTask: {
|
|
229
|
-
id:
|
|
230
|
-
title:
|
|
231
|
-
description:
|
|
232
|
-
category:
|
|
233
|
-
type:
|
|
253
|
+
id: "task-1",
|
|
254
|
+
title: "Existing task",
|
|
255
|
+
description: "",
|
|
256
|
+
category: "general",
|
|
257
|
+
type: "feature",
|
|
234
258
|
priority: 2,
|
|
235
|
-
status:
|
|
236
|
-
createdAt:
|
|
237
|
-
}
|
|
259
|
+
status: "task",
|
|
260
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
261
|
+
},
|
|
238
262
|
});
|
|
239
|
-
expect(screen.queryByText(
|
|
263
|
+
expect(screen.queryByText("Attach to workstream")).not.toBeInTheDocument();
|
|
240
264
|
});
|
|
241
|
-
it(
|
|
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(
|
|
245
|
-
const manualInput = screen.getByRole(
|
|
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,
|
|
273
|
+
await user.type(manualInput, "Platform Refinement");
|
|
248
274
|
expect(defaultProps.onWorkstreamInputChange).toHaveBeenCalled();
|
|
249
275
|
});
|
|
250
|
-
it(
|
|
276
|
+
it("renders custom taxonomy descriptions as helper text", () => {
|
|
251
277
|
renderForm({
|
|
252
|
-
taxonomies: [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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:
|
|
260
|
-
}
|
|
286
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
261
289
|
});
|
|
262
|
-
expect(screen.getByText(
|
|
290
|
+
expect(screen.getByText("Use this to track review routing.")).toBeInTheDocument();
|
|
263
291
|
});
|
|
264
|
-
it(
|
|
292
|
+
it("hides custom taxonomies from the task form when form visibility is disabled", () => {
|
|
265
293
|
renderForm({
|
|
266
|
-
taxonomies: [
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
taxonomies: [
|
|
295
|
+
{
|
|
296
|
+
id: "review-status",
|
|
297
|
+
label: "Review Status",
|
|
269
298
|
formEnabled: false,
|
|
270
|
-
widgetType:
|
|
299
|
+
widgetType: "select",
|
|
271
300
|
isSystem: false,
|
|
272
301
|
multiSelect: false,
|
|
273
|
-
options: [{ value:
|
|
274
|
-
}
|
|
302
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
275
305
|
});
|
|
276
|
-
expect(screen.queryByText(
|
|
306
|
+
expect(screen.queryByText("Review Status")).not.toBeInTheDocument();
|
|
277
307
|
});
|
|
278
|
-
it(
|
|
308
|
+
it("keeps a hidden custom taxonomy visible when the current task already has a value", () => {
|
|
279
309
|
renderForm({
|
|
280
|
-
formTaxonomies: {
|
|
281
|
-
taxonomies: [
|
|
282
|
-
|
|
283
|
-
|
|
310
|
+
formTaxonomies: { "review-status": "pending" },
|
|
311
|
+
taxonomies: [
|
|
312
|
+
{
|
|
313
|
+
id: "review-status",
|
|
314
|
+
label: "Review Status",
|
|
284
315
|
formEnabled: false,
|
|
285
|
-
widgetType:
|
|
316
|
+
widgetType: "select",
|
|
286
317
|
isSystem: false,
|
|
287
318
|
multiSelect: false,
|
|
288
|
-
options: [{ value:
|
|
289
|
-
}
|
|
319
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
320
|
+
},
|
|
321
|
+
],
|
|
290
322
|
});
|
|
291
|
-
expect(screen.getByText(
|
|
323
|
+
expect(screen.getByText("Review Status")).toBeInTheDocument();
|
|
292
324
|
});
|
|
293
|
-
it(
|
|
325
|
+
it("renders multi-select custom taxonomies as selectable chips", async () => {
|
|
294
326
|
const user = userEvent.setup();
|
|
295
327
|
renderForm({
|
|
296
|
-
taxonomies: [
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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:
|
|
304
|
-
{ value:
|
|
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(
|
|
309
|
-
expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith(
|
|
342
|
+
await user.click(screen.getByRole("button", { name: "Email" }));
|
|
343
|
+
expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith("channels", [
|
|
344
|
+
"email",
|
|
345
|
+
]);
|
|
310
346
|
});
|
|
311
|
-
it(
|
|
347
|
+
it("keeps selected retired multi-select custom taxonomy options available", () => {
|
|
312
348
|
renderForm({
|
|
313
|
-
formTaxonomies: { channels: [
|
|
314
|
-
taxonomies: [
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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:
|
|
322
|
-
{ value:
|
|
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(
|
|
327
|
-
expect(screen.getByRole(
|
|
364
|
+
expect(screen.getByRole("button", { name: /Email \(Retired\)/i })).toBeInTheDocument();
|
|
365
|
+
expect(screen.getByRole("button", { name: "Social" })).toBeInTheDocument();
|
|
328
366
|
});
|
|
329
|
-
it(
|
|
367
|
+
it("hides retired custom taxonomies from new-task entry when there is no selected value", () => {
|
|
330
368
|
renderForm({
|
|
331
|
-
taxonomies: [
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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:
|
|
339
|
-
}
|
|
377
|
+
options: [{ value: "email", label: "Email", status: "active" }],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
340
380
|
});
|
|
341
|
-
expect(screen.queryByText(
|
|
381
|
+
expect(screen.queryByText("Channels")).not.toBeInTheDocument();
|
|
342
382
|
});
|
|
343
|
-
it(
|
|
383
|
+
it("keeps retired custom taxonomies visible when the current task already has a value", () => {
|
|
344
384
|
renderForm({
|
|
345
|
-
formTaxonomies: { channels:
|
|
346
|
-
taxonomies: [
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
354
|
-
}
|
|
394
|
+
options: [{ value: "email", label: "Email", status: "active" }],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
355
397
|
});
|
|
356
|
-
expect(screen.getByText(
|
|
398
|
+
expect(screen.getByText("Channels")).toBeInTheDocument();
|
|
357
399
|
});
|
|
358
400
|
// --- Edit Task Tests ---
|
|
359
|
-
it(
|
|
360
|
-
renderForm({ editingTaskId:
|
|
361
|
-
expect(screen.getByDisplayValue(
|
|
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(
|
|
405
|
+
it("3.11 ID badge visible in edit mode", () => {
|
|
364
406
|
// Form-level edit mode validation.
|
|
365
|
-
renderForm({ editingTaskId:
|
|
407
|
+
renderForm({ editingTaskId: "123" });
|
|
366
408
|
expect(screen.getByText(/Activity & Comments/)).toBeInTheDocument();
|
|
367
409
|
});
|
|
368
|
-
it(
|
|
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(
|
|
413
|
+
expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
|
|
372
414
|
});
|
|
373
|
-
it(
|
|
415
|
+
it("renders assignee audit activity with readable labels instead of raw ids", () => {
|
|
374
416
|
renderForm({
|
|
375
|
-
editingTaskId:
|
|
417
|
+
editingTaskId: "123",
|
|
376
418
|
showComments: true,
|
|
377
419
|
assigneeOptions: [
|
|
378
420
|
...defaultProps.assigneeOptions,
|
|
379
|
-
{
|
|
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:
|
|
383
|
-
title:
|
|
430
|
+
id: "123",
|
|
431
|
+
title: "Existing Task",
|
|
384
432
|
activity: [
|
|
385
433
|
{
|
|
386
|
-
id:
|
|
387
|
-
type:
|
|
388
|
-
timestamp:
|
|
434
|
+
id: "event:task-event-1",
|
|
435
|
+
type: "event",
|
|
436
|
+
timestamp: "2026-03-23T12:00:00.000Z",
|
|
389
437
|
event: {
|
|
390
|
-
id:
|
|
391
|
-
taskId:
|
|
392
|
-
workspaceId:
|
|
393
|
-
action:
|
|
394
|
-
actor:
|
|
395
|
-
actorType:
|
|
396
|
-
createdAt:
|
|
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:
|
|
401
|
-
to:
|
|
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(
|
|
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(
|
|
461
|
+
it("renders planning relationship audit activity with reference labels instead of raw ids", () => {
|
|
414
462
|
renderForm({
|
|
415
|
-
editingTaskId:
|
|
463
|
+
editingTaskId: "123",
|
|
416
464
|
showComments: true,
|
|
417
465
|
currentTask: {
|
|
418
|
-
id:
|
|
419
|
-
title:
|
|
466
|
+
id: "123",
|
|
467
|
+
title: "Existing Task",
|
|
420
468
|
activity: [
|
|
421
469
|
{
|
|
422
|
-
id:
|
|
423
|
-
type:
|
|
424
|
-
timestamp:
|
|
470
|
+
id: "event:task-event-relationship",
|
|
471
|
+
type: "event",
|
|
472
|
+
timestamp: "2026-03-23T12:00:00.000Z",
|
|
425
473
|
event: {
|
|
426
|
-
id:
|
|
427
|
-
taskId:
|
|
428
|
-
workspaceId:
|
|
429
|
-
action:
|
|
430
|
-
actor:
|
|
431
|
-
actorType:
|
|
432
|
-
createdAt:
|
|
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:
|
|
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(
|
|
494
|
+
expect(screen.getByText("Workstream: none -> WS-12")).toBeInTheDocument();
|
|
447
495
|
expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
|
|
448
496
|
});
|
|
449
|
-
it(
|
|
497
|
+
it("3.13 Update title", async () => {
|
|
450
498
|
const user = userEvent.setup();
|
|
451
|
-
renderForm({ editingTaskId:
|
|
452
|
-
const input = screen.getByDisplayValue(
|
|
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,
|
|
502
|
+
await user.type(input, "New Title");
|
|
455
503
|
expect(defaultProps.onTitleChange).toHaveBeenCalled();
|
|
456
504
|
});
|
|
457
|
-
it(
|
|
505
|
+
it("keeps scheduled date editable in the task form", () => {
|
|
458
506
|
renderForm({
|
|
459
|
-
editingTaskId:
|
|
460
|
-
currentTask: { id:
|
|
507
|
+
editingTaskId: "parent-1",
|
|
508
|
+
currentTask: { id: "parent-1", title: "Parent Task" },
|
|
461
509
|
});
|
|
462
|
-
const scheduledLabel = screen.getByText(
|
|
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(
|
|
514
|
+
throw new Error("Scheduled date input missing");
|
|
467
515
|
expect(scheduledInput.disabled).toBe(false);
|
|
468
|
-
expect(screen.queryByText(
|
|
516
|
+
expect(screen.queryByText("Parent tasks cannot be scheduled. Schedule leaf tasks instead.")).not.toBeInTheDocument();
|
|
469
517
|
});
|
|
470
|
-
it(
|
|
518
|
+
it("3.14 Context Documents section renders", async () => {
|
|
471
519
|
const user = userEvent.setup();
|
|
472
520
|
renderForm();
|
|
473
|
-
expect(screen.getByText(
|
|
474
|
-
await user.click(screen.getByTitle(
|
|
475
|
-
expect(screen.getByRole(
|
|
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(
|
|
525
|
+
it("3.15 Markdown Help Toggle", async () => {
|
|
478
526
|
const user = userEvent.setup();
|
|
479
527
|
renderForm();
|
|
480
|
-
const helpBtn = screen.getByTitle(
|
|
528
|
+
const helpBtn = screen.getByTitle("Markdown Help");
|
|
481
529
|
await user.click(helpBtn);
|
|
482
530
|
expect(defaultProps.onShowMarkdownHelpChange).toHaveBeenCalledWith(true);
|
|
483
531
|
});
|
|
484
|
-
it(
|
|
532
|
+
it("3.16 Description Focus", () => {
|
|
485
533
|
renderForm();
|
|
486
|
-
renderForm({ description:
|
|
487
|
-
const previewDiv = screen.getByText(
|
|
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(
|
|
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(
|
|
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(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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(
|
|
622
|
+
it("3.24 Add comment", async () => {
|
|
544
623
|
const user = userEvent.setup();
|
|
545
|
-
renderForm({ editingTaskId:
|
|
546
|
-
|
|
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(
|
|
628
|
+
it("renders named AI comment authors from resolved actor data", () => {
|
|
556
629
|
renderForm({
|
|
557
|
-
editingTaskId:
|
|
630
|
+
editingTaskId: "123",
|
|
558
631
|
showComments: true,
|
|
559
632
|
comments: [
|
|
560
633
|
{
|
|
561
|
-
id:
|
|
562
|
-
author:
|
|
563
|
-
text:
|
|
564
|
-
timestamp:
|
|
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:
|
|
567
|
-
kind:
|
|
568
|
-
label:
|
|
569
|
-
username:
|
|
570
|
-
icon:
|
|
571
|
-
color:
|
|
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(
|
|
578
|
-
expect(screen.queryByText(
|
|
650
|
+
expect(screen.getByText("Codex")).toBeInTheDocument();
|
|
651
|
+
expect(screen.queryByText("AI Agent")).not.toBeInTheDocument();
|
|
579
652
|
});
|
|
580
|
-
it(
|
|
653
|
+
it("renders task audit events in the activity feed", () => {
|
|
581
654
|
renderForm({
|
|
582
|
-
editingTaskId:
|
|
655
|
+
editingTaskId: "123",
|
|
583
656
|
showComments: true,
|
|
584
657
|
currentTask: {
|
|
585
|
-
id:
|
|
586
|
-
title:
|
|
587
|
-
status:
|
|
588
|
-
createdAt:
|
|
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:
|
|
592
|
-
type:
|
|
593
|
-
timestamp:
|
|
664
|
+
id: "event:1",
|
|
665
|
+
type: "event",
|
|
666
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
594
667
|
event: {
|
|
595
|
-
id:
|
|
596
|
-
taskId:
|
|
597
|
-
workspaceId:
|
|
598
|
-
action:
|
|
599
|
-
actor:
|
|
600
|
-
actorType:
|
|
601
|
-
createdAt:
|
|
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:
|
|
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(
|
|
613
|
-
expect(screen.queryByText(
|
|
685
|
+
expect(screen.getByText("Status: task -> in-progress")).toBeInTheDocument();
|
|
686
|
+
expect(screen.queryByText("System")).not.toBeInTheDocument();
|
|
614
687
|
});
|
|
615
|
-
it(
|
|
688
|
+
it("summarizes description audit events without rendering the full description body", () => {
|
|
616
689
|
renderForm({
|
|
617
|
-
editingTaskId:
|
|
690
|
+
editingTaskId: "123",
|
|
618
691
|
showComments: true,
|
|
619
692
|
currentTask: {
|
|
620
|
-
id:
|
|
621
|
-
title:
|
|
622
|
-
status:
|
|
623
|
-
createdAt:
|
|
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:
|
|
627
|
-
type:
|
|
628
|
-
timestamp:
|
|
699
|
+
id: "event:2",
|
|
700
|
+
type: "event",
|
|
701
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
629
702
|
event: {
|
|
630
|
-
id:
|
|
631
|
-
taskId:
|
|
632
|
-
workspaceId:
|
|
633
|
-
action:
|
|
634
|
-
actor:
|
|
635
|
-
actorType:
|
|
636
|
-
createdAt:
|
|
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:
|
|
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(
|
|
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(
|
|
728
|
+
it("summarizes title audit events without rendering before and after title text", () => {
|
|
656
729
|
renderForm({
|
|
657
|
-
editingTaskId:
|
|
730
|
+
editingTaskId: "123",
|
|
658
731
|
showComments: true,
|
|
659
732
|
currentTask: {
|
|
660
|
-
id:
|
|
661
|
-
title:
|
|
662
|
-
status:
|
|
663
|
-
createdAt:
|
|
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:
|
|
667
|
-
type:
|
|
668
|
-
timestamp:
|
|
739
|
+
id: "event:3",
|
|
740
|
+
type: "event",
|
|
741
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
669
742
|
event: {
|
|
670
|
-
id:
|
|
671
|
-
taskId:
|
|
672
|
-
workspaceId:
|
|
673
|
-
action:
|
|
674
|
-
actor:
|
|
675
|
-
actorType:
|
|
676
|
-
createdAt:
|
|
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:
|
|
681
|
-
to:
|
|
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(
|
|
691
|
-
expect(screen.queryByText(
|
|
692
|
-
expect(screen.queryByText(
|
|
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(
|
|
767
|
+
it("falls back to the current task actor label for AI comments when only the id is present", () => {
|
|
695
768
|
renderForm({
|
|
696
|
-
editingTaskId:
|
|
769
|
+
editingTaskId: "123",
|
|
697
770
|
showComments: true,
|
|
698
771
|
currentTask: {
|
|
699
|
-
id:
|
|
700
|
-
title:
|
|
701
|
-
assignee:
|
|
772
|
+
id: "123",
|
|
773
|
+
title: "Codex-owned task",
|
|
774
|
+
assignee: "ai-profile-123",
|
|
702
775
|
assigneeActor: {
|
|
703
|
-
id:
|
|
704
|
-
kind:
|
|
705
|
-
label:
|
|
706
|
-
username:
|
|
707
|
-
icon:
|
|
708
|
-
color:
|
|
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:
|
|
715
|
-
author:
|
|
716
|
-
text:
|
|
717
|
-
timestamp:
|
|
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(
|
|
794
|
+
expect(screen.getByText("Codex")).toBeInTheDocument();
|
|
722
795
|
expect(screen.queryByText(/019d|ai-profile/i)).not.toBeInTheDocument();
|
|
723
796
|
});
|
|
724
|
-
it(
|
|
797
|
+
it("uses the resolved actor icon and color tint for named AI comments", () => {
|
|
725
798
|
const { container } = renderForm({
|
|
726
|
-
editingTaskId:
|
|
799
|
+
editingTaskId: "123",
|
|
727
800
|
showComments: true,
|
|
728
801
|
comments: [
|
|
729
802
|
{
|
|
730
|
-
id:
|
|
731
|
-
author:
|
|
732
|
-
text:
|
|
733
|
-
timestamp:
|
|
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:
|
|
736
|
-
kind:
|
|
737
|
-
label:
|
|
738
|
-
username:
|
|
739
|
-
icon:
|
|
740
|
-
color:
|
|
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(
|
|
747
|
-
const bubble = screen
|
|
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:
|
|
824
|
+
borderColor: "rgba(245, 158, 11, 0.35)",
|
|
750
825
|
});
|
|
751
826
|
});
|
|
752
827
|
// --- Context Upload ---
|
|
753
|
-
it(
|
|
828
|
+
it("3.25 Render context image", async () => {
|
|
754
829
|
const user = userEvent.setup();
|
|
755
|
-
renderForm({ contextFiles: [{ path:
|
|
756
|
-
await user.click(screen.getByTitle(
|
|
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(
|
|
834
|
+
expect(img).toHaveAttribute("src", "img.png");
|
|
760
835
|
});
|
|
761
|
-
it(
|
|
836
|
+
it("3.26 Remove context file", async () => {
|
|
762
837
|
const user = userEvent.setup();
|
|
763
|
-
renderForm({ contextFiles: [{ path:
|
|
764
|
-
await user.click(screen.getByTitle(
|
|
765
|
-
const deleteBtn = screen.getByTitle(
|
|
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
|
});
|